diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..12fc7bf --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,24 @@ +name: Tests + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: 3.14 + + - name: Install dependencies + run: | + pip install -e . + pip install pytest + + - name: Run tests + run: pytest -v diff --git a/.gitignore b/.gitignore index fb3127d..ea2a216 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ dist/ mama/mama.spec mama.cmake + +packages/ +bin/ diff --git a/README.md b/README.md index 3a62f0b..6473e76 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Each mama build target exports CMake `${ProjectName}_INCLUDES` and `${ProjectNam are gathered in correct linker order inside `MAMA_INCLUDES` and `MAMA_LIBS`. This ensures the least amount of friction for developers - everything just works. -There is no central package repository, all packages are pulled and updated from public or +There is no central package repository, all packages are pulled and updated from public or private git repositories. Package versioning is done through git tags or branches. Custom build systems are also supported. For additional documentation explore: [build_target.py](mama/build_target.py) @@ -219,7 +219,7 @@ And then rebuilding with an artifactory package available $ mama rebuild googletest ========= Mama Build Tool ========== - Target googletest CLEAN linux - - Target googletest BUILD [cleaned target] + - Target googletest BUILD [cleaned target] Artifactory fetch ftp.myartifactory.com/googletest-linux-x64-release-ebb36f3 770.6KB |<==================================================| 100 % Artifactory unzip googletest-linux-x64-release-ebb36f3 @@ -233,11 +233,29 @@ $ mama rebuild googletest ## For Mama Contributors We are open for any improvements and feedback via pull requests. +### Development Setup The package `setuptools>=65.0` is required, ensure the version is correct with `pip3 show setuptools`. You can set up local development with `$ pip3 install -e . --no-cache-dir` but make sure you have latest setuptools (>65.0) and latest pip3 (>22.3). This command will fail with older toolkits. -Uploading a source distributionP: +### Running Tests + +Install pytest and run all tests from the project root: + +```bash +uv venv +uv pip install pytest +uv run pytest +``` + +Or to run a specific test: + +```bash +pytest tests/test_git_pinning/ +``` + +### Publishing +Uploading a source distribution: 1. Get dependencies: `pip3 install build twine` 2. Build sdist: `python -m build` 3. Upload with twine: `twine upload --skip-existing dist/*` diff --git a/mama/build_target.py b/mama/build_target.py index 070657d..400e65d 100644 --- a/mama/build_target.py +++ b/mama/build_target.py @@ -37,11 +37,11 @@ class BuildTarget: Customization points: ``` class MyProject(mama.BuildTarget): - + workspace = 'build' def configure(self): - self.add_git('ReCpp', + self.add_git('ReCpp', 'http://github.com/RedFox20/ReCpp.git') def configure(self): @@ -129,9 +129,9 @@ def source_dir(self, subpath=''): """ Returns the current source directory. ``` - self.source_dir() + self.source_dir() # --> C:/Projects/ReCpp - self.source_dir('lib/ReCpp.lib') + self.source_dir('lib/ReCpp.lib') # --> C:/Projects/ReCpp/lib/ReCpp.lib ``` """ @@ -143,7 +143,7 @@ def build_dir(self, subpath=''): """ Returns the current build directory. ``` - self.build_dir() + self.build_dir() # --> C:/Projects/ReCpp/build/windows self.build_dir('lib/ReCpp.lib') # --> C:/Projects/ReCpp/build/windows/lib/ReCpp.lib @@ -201,12 +201,12 @@ def add_local(self, name, source_dir, mamafile=None, always_build=False, args=[] return self.dep.add_child(LocalSource(name, source_dir, mamafile, always_build, args)) - def add_git(self, name, git_url, git_branch='', git_tag='', mamafile=None, shallow=True, args=[]) -> BuildDependency: + def add_git(self, name, git_url, git_branch='', git_tag='', git_commit='', mamafile=None, shallow=True, args=[]) -> BuildDependency: """ Add a remote GIT dependency. The dependency will be cloned and updated according to mamabuild. Use `mama update` to force update the git repositories. - + If the remote GIT repository does not contain a `mamafile.py`, you will have to provide your own relative or absolute mamafile path. @@ -216,12 +216,16 @@ def add_git(self, name, git_url, git_branch='', git_tag='', mamafile=None, shall ``` self.add_git('ReCpp', 'git@github.com:RedFox20/ReCpp.git') self.add_git('ReCpp', 'git@github.com:RedFox20/ReCpp.git', git_branch='master') - self.add_git('opencv', 'https://github.com/opencv/opencv.git', + self.add_git('opencv', 'https://github.com/opencv/opencv.git', git_branch='3.4', mamafile='mama/opencv_cfg.py') ``` """ if self.dep.from_artifactory: # already loaded from artifactory? return self.get_dependency(name) + + if git_tag == '' and git_commit != '': + git_tag = git_commit + return self.dep.add_child(Git(name, git_url, git_branch, git_tag, mamafile, shallow, args)) @@ -301,7 +305,7 @@ def inject_products(self, dst_dep, src_dep, include_path, libs, libfilters=None) Name of defines is given via `include_path` and `libs` params. `libfilters` does simple string matching; if nothing matches, the first export lib is chosen. ``` - self.inject_products('libpng', 'zlib', + self.inject_products('libpng', 'zlib', 'ZLIB_INCLUDE_DIR', 'ZLIB_LIBRARY', 'zlibstatic') ``` @@ -326,7 +330,7 @@ def get_product_defines(self): Returns a list of injected defines: ``` defines = self.get_product_defines() - # --> [ 'ZLIB_INCLUDE_DIR=path/to/zlib/include', + # --> [ 'ZLIB_INCLUDE_DIR=path/to/zlib/include', # 'ZLIB_LIBRARY=path/to/lib/zlib.a', ... ] ``` """ @@ -378,9 +382,9 @@ def add_build_dependency(self, all=None, windows=None, linux=None, macos=None, i Manually add a build dependency to prevent unnecessary rebuilds. @note Normally the build dependency is detected from the packaged libraries. - + if the dependency file does not exist, then the project will be rebuilt - + if your project has no build dependencies, it will always be rebuilt, so make sure to add_build_dependency or export_lib ``` @@ -425,7 +429,7 @@ def package(self): def export_include(self, include_path, build_dir=False): """ CUSTOM PACKAGE INCLUDES (if self.default_package() is insufficient). - + Export include path relative to source directory OR if build_dir=True, then relative to build directory. ``` self.export_include('include') # MyRepo/include @@ -440,7 +444,7 @@ def export_include(self, include_path, build_dir=False): def export_includes(self, include_paths=[''], build_dir=False): """ CUSTOM PACKAGE INCLUDES (if self.default_package() is insufficient) - + Export include paths relative to source directory OR if build_dir=True, then relative to build directory Example: @@ -455,7 +459,7 @@ def export_includes(self, include_paths=[''], build_dir=False): def export_lib(self, relative_path, src_dir=False, build_dir=True): """ CUSTOM PACKAGE LIBS (if self.default_package() is insufficient) - + Export a specific lib relative to build directory OR if src_dir=True, then relative to source directory Example: @@ -472,17 +476,17 @@ def export_lib(self, relative_path, src_dir=False, build_dir=True): def export_libs(self, path = '.', pattern_substrings = ['.lib', '.a'], src_dir=False, build_dir=True, order=None): """ CUSTOM PACKAGE LIBS (if self.default_package() is insufficient) - + Export several libs relative to build directory using EXTENSION MATCHING OR if src_dir=True, then relative to source directory - + Example: ``` self.export_libs() # gather any .lib or .a from build dir self.export_libs('.', ['.dll', '.so']) # gather any .dll or .so from build dir self.export_libs('lib', src_dir=True) # export everything from project/lib directory self.export_libs('external/lib') # gather specific static libs from build dir - + # export the libs in a particular order for Linux linker self.export_libs('lib', order=[ 'xphoto', 'calib3d', 'flann', 'core' @@ -501,7 +505,7 @@ def export_asset(self, asset, category=None, src_dir=True, build_dir=False): This can be later used when creating a deployment category -- (optional) Can be used for grouping the assets and flattening folder structure - + Example: ``` self.export_asset('extras/csharp/NanoMesh.cs') @@ -522,12 +526,12 @@ def export_assets(self, assets_path: str, pattern_substrings = [], category=None This can be later used when creating a deployment category -- (optional) Can be used for grouping the assets and flattening folder structure - + Example: ``` self.export_assets('extras/csharp', ['.cs']) --> {deploy}/extras/csharp/NanoMesh.cs - + self.export_assets('extras/csharp', ['.cs'], category='dotnet') --> {deploy}/dotnet/NanoMesh.cs ``` @@ -563,7 +567,7 @@ def inject_env(self): ``` def build(self): self.inject_env() # prepare platform - self.my_custom_build() # + self.my_custom_build() # ``` """ cmake.inject_env(self) @@ -614,7 +618,7 @@ def add_c_flags(self, *flags): for flag in flags: if isinstance(flag, list): self.add_c_flags(*flag) else: self._add_dict_flag(self.cmake_cflags, flag) - + def add_cl_flags(self, *flags): """ @@ -670,7 +674,7 @@ def add_platform_ld_flags(self, windows=None, linux=None, macos=None, ios=None, Adds linker flags depending on configuration platform. Supports many different usages: strings, list of strings, or space separate string. ``` - self.add_platform_ld_flags(windows='/LTCG', + self.add_platform_ld_flags(windows='/LTCG', ios=['-lobjc', '-rdynamic'], linux='-rdynamic -s') ``` @@ -803,7 +807,7 @@ def copy(self, src: str, dst: str, filter: list = None): Utility for copying files and folders ``` # copies built .so into an android archive - self.copy(self.build_dir('libAwesome.so'), + self.copy(self.build_dir('libAwesome.so'), self.source_dir('deploy/Awesome.aar/jni/armeabi-v7a')) ``` - filter: can be a string or list of strings to filter files by suffix @@ -859,7 +863,7 @@ def download_and_unzip(self, remote_zip: str, extract_dir: str, unless_file_exis unless_file_exists -- If the specified file exists, then download and unzip steps are skipped. ``` - self.download_and_unzip('http://example.com/archive.zip', + self.download_and_unzip('http://example.com/archive.zip', 'bin', 'bin/unzipped_file.txt') # --> 'bin/' on success # --> None on failure @@ -982,7 +986,7 @@ def run_program(self, working_dir: str, command: str, exit_on_fail=True, env=Non """ Run any program in any directory. Can be used for custom tools. ``` - self.run_program(self.source_dir('bin'), + self.run_program(self.source_dir('bin'), self.source_dir('bin/DbTool')) ``` """ @@ -1019,7 +1023,7 @@ def gtest(self, executable: str, args: str, src_dir=True, gdb=False): The gtest report is written to $src_dir/test/report.xml. Arguments - executable -- which executable to run - - args -- a string of options separated by spaces, + - args -- a string of options separated by spaces, 'gdb', 'nogdb' or gtest fixture/test partial name - src_dir -- [True] If true, then executable is relative to source directory. - gdb -- [False] If true, then run with gdb. @@ -1133,8 +1137,8 @@ def package(self): # custom export AGL as include from source folder self.export_includes(['AGL']) # custom export any .lib or .a from build folder - self.export_libs('.', ['.lib', '.a']) - + self.export_libs('.', ['.lib', '.a']) + if self.windows: self.export_syslib('opengl32.lib') @@ -1143,7 +1147,7 @@ def package(self): ``` """ pass - + def default_package(self): """ @@ -1235,7 +1239,7 @@ def papa_deploy(self, package_path, src_dir=False, MyPackageName/libawesome.so MyPackageName/include/... MyPackageName/someassets/extra.txt - + PAPA descriptor `papa.txt` format: P MyPackageName I include @@ -1281,7 +1285,7 @@ def start(self, args): """ pass - + ############################################ @@ -1502,4 +1506,3 @@ def build(self): ###################################################################################### - \ No newline at end of file diff --git a/mama/types/git.py b/mama/types/git.py index 5a6ca43..11d1f50 100644 --- a/mama/types/git.py +++ b/mama/types/git.py @@ -81,6 +81,9 @@ def get_current_repository_commit(dep: BuildDependency): console(f' {dep.name: <16} git show --format=%h -s: {result}') return result + @staticmethod + def is_hex_string(s: str) -> bool: + return len(s) > 0 and all(c in string.hexdigits for c in s) def init_commit_hash(self, dep: BuildDependency, use_cache: bool, fetch_remote: bool): """ @@ -98,7 +101,7 @@ def init_commit_hash(self, dep: BuildDependency, use_cache: bool, fetch_remote: return result # is the tag actually a commit hash? - if self.tag and all(c in string.hexdigits for c in self.tag): + if Git.is_hex_string(self.tag): if dep.config.verbose: console(f' {self.name} using tag as the commit hash: {self.tag}') return self.tag @@ -128,16 +131,27 @@ def init_commit_hash(self, dep: BuildDependency, use_cache: bool, fetch_remote: def fetch_origin(self, dep: BuildDependency): - self.run_git(dep, f"pull origin {self.branch_or_tag()} -q") + branch = self.branch_or_tag() + if not Git.is_hex_string(branch): + if self.tag: + self.run_git(dep, f"fetch origin tag {branch} -q") + else: + # detached head cant perform this, e.g when switching from tag pin previously + result = self.run_git(dep, f"pull origin {branch} -q", throw=False) + if result != 0: + self.run_git(dep, f"fetch origin {branch} -q") def git_status_file(self, dep: BuildDependency): return path_join(dep.build_dir, 'git_status') + @staticmethod + def format_git_status(url: str, tag: str, branch: str, commit: str): + return f"{url}\n{tag}\n{branch}\n{commit}\n" def save_status(self, dep: BuildDependency): commit = self.get_commit_hash(dep) - status = f"{self.url}\n{self.tag}\n{self.branch}\n{commit}\n" + status = self.format_git_status(self.url, self.tag, self.branch, commit) if save_file_if_contents_changed(self.git_status_file(dep), status): if dep.config.verbose: console(f' {self.name} write git status commit={commit}') @@ -187,12 +201,22 @@ def branch_or_tag(self): return '' - def checkout_current_branch(self, dep: BuildDependency): + def checkout_current_branch_or_tag(self, dep: BuildDependency, is_commit_pin=False): branch = self.branch_or_tag() if branch: if self.tag and self.tag_changed: self.run_git(dep, "reset --hard") - self.run_git(dep, f"checkout {branch}") + if is_commit_pin: + self.run_git(dep, f"fetch --depth 1 origin {branch}") + self.run_git(dep, f"checkout {branch}") + elif self.branch: + # hacky fix + self.run_git(dep, f"fetch origin +refs/heads/{branch}:refs/remotes/origin/{branch}", throw=False) + self.run_git(dep, f"checkout -B {branch} origin/{branch}") + else: # tag + if self.tag_changed: + self.run_git(dep, f"fetch origin tag {branch}") + self.run_git(dep, f"checkout {branch}") def reclone_wipe(self, dep: BuildDependency): @@ -270,22 +294,28 @@ def clone_or_pull(self, dep: BuildDependency, wiped=False): if is_dir_empty(dep.src_dir): if not wiped and dep.config.print: console(f" - Target {dep.name: <16} CLONE because src is missing", color=Color.BLUE) - branch = self.branch_or_tag() - if branch: branch = f" --branch {self.branch_or_tag()}" + br_or_tag = self.branch_or_tag() + is_commit_pin = Git.is_hex_string(br_or_tag) + checkout_branch = '' if is_commit_pin or len(br_or_tag) == 0 else f' --branch {br_or_tag}' depth = '' if unshallow else '--depth 1' - clone_args = f"--recurse-submodules {depth} {branch} {self.url}" + clone_args = f"--recurse-submodules {depth} {checkout_branch} {self.url}" self.clone_with_filtered_progress(dep, clone_args, dep.src_dir) - self.checkout_current_branch(dep) + self.checkout_current_branch_or_tag(dep, is_commit_pin=is_commit_pin) else: if dep.config.print: console(f" - Pulling {dep.name: <16} SCM change detected", color=Color.BLUE) if unshallow: self.unshallow(dep) - self.checkout_current_branch(dep) + is_commit_pin = Git.is_hex_string(self.branch_or_tag()) + self.checkout_current_branch_or_tag(dep, is_commit_pin=is_commit_pin) self.run_git(dep, 'submodule update --init --recursive') if not self.tag: # pull if not a tag - self.run_git(dep, "reset --hard -q") - self.run_git(dep, "pull") + if self.branch: + self.run_git(dep, f"fetch origin {self.branch} -q", throw=False) + self.run_git(dep, f"reset --hard origin/{self.branch} -q") + else: + self.run_git(dep, "fetch -q", throw=False) + self.run_git(dep, "reset --hard @{upstream} -q") # https://git-scm.com/docs/gitrevisions#Documentation/gitrevisions.txt-branchnameupstreamegmasterupstreamu def unshallow(self, dep: BuildDependency): @@ -305,7 +335,7 @@ def unshallow(self, dep: BuildDependency): console(f' - Unshallowing {dep.name}', color=Color.YELLOW) self.run_git(dep, 'config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"') self.run_git(dep, 'remote update') - # this last step is allowed to fail, just in case it was + # this last step is allowed to fail, just in case it was # semi-shallow (history was complete, but remote refs were shallow) self.run_git(dep, 'fetch --unshallow', throw=False) diff --git a/mama/util.py b/mama/util.py index 1b2d437..24d4d64 100644 --- a/mama/util.py +++ b/mama/util.py @@ -42,7 +42,7 @@ def copy_files(fromFolder: str, toFolder: str, fileNames: List[str]): def deploy_framework(framework: str, deployFolder: str): if not os.path.exists(framework): - raise IOError(f'no framework found at: {framework}') + raise IOError(f'no framework found at: {framework}') if os.path.exists(deployFolder): name = os.path.basename(framework) deployPath = os.path.join(deployFolder, name) @@ -434,7 +434,11 @@ def copy_dir(src_dir: str, out_dir: str, filter: list = None) -> bool: os.makedirs(out_dir, exist_ok=True) copied = False root = os.path.dirname(src_dir) - for fulldir, _, files in os.walk(src_dir): + norm_out = os.path.normcase(os.path.normpath(out_dir)) + for fulldir, dirs, files in os.walk(src_dir): + # skip the output directory to prevent infinite recursion + dirs[:] = [d for d in dirs if os.path.normcase(os.path.normpath(os.path.join(fulldir, d))) != norm_out] + reldir = fulldir[len(root):].lstrip('\\/') if reldir: dst_folder = os.path.join(out_dir, reldir) diff --git a/pyproject.toml b/pyproject.toml index 594a21d..8f03564 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,9 @@ classifiers = [ [project.scripts] mama = "mama.main:main" +[tool.pytest.ini_options] +testpaths = ["tests"] + [tool.setuptools.dynamic] version = {attr = "mama._version.__version__"} diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..5793767 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,5 @@ +import os +import sys + +# Add the tests directory to sys.path so test files can import testutils directly +sys.path.insert(0, os.path.dirname(__file__)) diff --git a/tests/test/example_consumer/CMakeLists.txt b/tests/test/example_consumer/CMakeLists.txt index 5691f30..ae2a168 100644 --- a/tests/test/example_consumer/CMakeLists.txt +++ b/tests/test/example_consumer/CMakeLists.txt @@ -1,12 +1,10 @@ -cmake_minimum_required(VERSION 3.6) +cmake_minimum_required(VERSION 3.20) project(example_consumer) include(mama.cmake) include_directories(${MAMA_INCLUDES}) -file(GLOB_RECURSE EXAMPLE_CONSUMER_SOURCES *.cpp *.h) -source_group(ExampleConsumer FILES ${EXAMPLE_CONSUMER_SOURCES}) -add_executable(ExampleConsumer ${EXAMPLE_CONSUMER_SOURCES}) +add_executable(ExampleConsumer consumer.cpp) target_link_libraries(ExampleConsumer ${MAMA_LIBS}) install(TARGETS ExampleConsumer DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/bin) diff --git a/tests/test/example_consumer/mamafile.py b/tests/test/example_consumer/mamafile.py index b36eecf..b8443ee 100644 --- a/tests/test/example_consumer/mamafile.py +++ b/tests/test/example_consumer/mamafile.py @@ -1,11 +1,15 @@ import mama class ExampleConsumer(mama.BuildTarget): - workspace = 'wolf3d' + workspace = 'packages' + + def init(self): + self.prefer_gcc() def dependencies(self): self.add_local('ExampleLibrary', '../example_library') - self.add_git('ExampleRemote', 'https://github.com/RedFox20/MamaExampleRemote.git') + self.add_git('ExampleRemote', 'https://github.com/RedFox20/MamaExampleRemote.git', + git_tag='4acd9052f27a459314651dd485ae8fa79a04d49d') # optional: pre-build configuration step def configure(self): diff --git a/tests/test/example_library/CMakeLists.txt b/tests/test/example_library/CMakeLists.txt index 92e89f5..c43ccf2 100644 --- a/tests/test/example_library/CMakeLists.txt +++ b/tests/test/example_library/CMakeLists.txt @@ -1,6 +1,9 @@ -cmake_minimum_required(VERSION 3.6) +cmake_minimum_required(VERSION 3.20) project(example_library) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS ON) file(GLOB_RECURSE EXAMPLE_LIBRARY_SOURCES *.cpp *.h) source_group(ExampleLibrary FILES ${EXAMPLE_LIBRARY_SOURCES}) diff --git a/tests/test/example_library/library.cpp b/tests/test/example_library/library.cpp index 7bd61ab..169f14b 100644 --- a/tests/test/example_library/library.cpp +++ b/tests/test/example_library/library.cpp @@ -5,7 +5,7 @@ namespace example { // ensure C++17 features are used - namespace fs = std::experimental::filesystem; + namespace fs = std::filesystem; bool print_file_exists(const std::string& str) { diff --git a/tests/test_git_pin_change/mamafile.py b/tests/test_git_pin_change/mamafile.py new file mode 100644 index 0000000..4e43c0a --- /dev/null +++ b/tests/test_git_pin_change/mamafile.py @@ -0,0 +1,28 @@ +import mama +import os + +class test(mama.BuildTarget): + workspace = 'packages' + + def build(self): + self.nothing_to_build() + + def dependencies(self): + stage = os.environ.get('GIT_PIN_CHANGE_TEST') # Uses env variable to dynamically change the pinned commit + + remote_name = 'ExampleRemote' + remote_url = 'https://github.com/BatteredBunny/MamaExampleRemote.git' + + # Switches between having REMOTE_VERSION and not to demonstrate that the contents actually change + if stage == '0': + self.add_git(remote_name, remote_url, git_commit='4acd9052f27a459314651dd485ae8fa79a04d49d') # has no REMOTE_VERSION + elif stage == '1': + self.add_git(remote_name, remote_url, git_commit='993e326cf840bc2df9d67b14d6e2fe0d38736713') # has REMOTE_VERSION 2 + elif stage == '2': + self.add_git(remote_name, remote_url, git_tag='v1.0.0') # has no REMOTE_VERSION + elif stage == '3': + self.add_git(remote_name, remote_url, git_tag='v2.0.0') # has REMOTE_VERSION 2 + elif stage == '4': + self.add_git(remote_name, remote_url, git_branch='old') # Branched from old commit which has no REMOTE_VERSION + elif stage == '5': + self.add_git(remote_name, remote_url, git_branch='master') # Should go to latest commit which has REMOTE_VERSION 2 \ No newline at end of file diff --git a/tests/test_git_pin_change/test_git_pin_change.py b/tests/test_git_pin_change/test_git_pin_change.py new file mode 100644 index 0000000..08b3644 --- /dev/null +++ b/tests/test_git_pin_change/test_git_pin_change.py @@ -0,0 +1,24 @@ +import os +from testutils import init, shell_exec, file_contains + +def stage(num: int, expects: bool, assert_message: str = ""): + os.environ['GIT_PIN_CHANGE_TEST'] = str(num) + shell_exec("mama update") + + result = file_contains('packages/ExampleRemote/ExampleRemote/remote.h', 'REMOTE_VERSION') + + if expects: + assert result, assert_message + else: + assert not result, assert_message + +# Test that switches between having REMOTE_VERSION and not to demonstrate that the contents actually change when changing git_tag pins +def test_git_pin_change(): + init(__file__, clean_dirs=['packages']) + + stage(0, False, "Failed to pin to a specific commit") + stage(1, True, "Failed to update commit pin to a new commit") + stage(2, False, "Failed to switch from commit pin to tag pin") + stage(3, True, "Failed to update between tag pins") + stage(4, False, "Failed to switch from tag pin to branch pin") + stage(5, True, "Failed to update between branch pins") diff --git a/tests/test_git_pinning/mamafile.py b/tests/test_git_pinning/mamafile.py new file mode 100644 index 0000000..43ae688 --- /dev/null +++ b/tests/test_git_pinning/mamafile.py @@ -0,0 +1,13 @@ +import mama + +class test(mama.BuildTarget): + workspace = 'packages' + + def build(self): + self.nothing_to_build() + + def dependencies(self): + self.add_git('ExampleRemote', 'https://github.com/BatteredBunny/MamaExampleRemote.git', git_tag='v1.0.0') + self.add_git('ExampleRemote2', 'https://github.com/BatteredBunny/MamaExampleRemote.git', git_tag='v2.0.0') + self.add_git('ExampleRemote3', 'https://github.com/BatteredBunny/MamaExampleRemote.git', git_tag='4acd9052f27a459314651dd485ae8fa79a04d49d') + self.add_git('ExampleRemote4', 'https://github.com/BatteredBunny/MamaExampleRemote.git', git_tag='993e326cf840bc2df9d67b14d6e2fe0d38736713') \ No newline at end of file diff --git a/tests/test_git_pinning/test_git_pinning.py b/tests/test_git_pinning/test_git_pinning.py new file mode 100644 index 0000000..4ad97c5 --- /dev/null +++ b/tests/test_git_pinning/test_git_pinning.py @@ -0,0 +1,15 @@ +from testutils import init, shell_exec, file_contains + +def remote_file_contains(dep_name, text): + return file_contains(f'packages/{dep_name}/{dep_name}/remote.h', text) + +# Make sure different git pinning methods work +def test_git_pinning(): + init(__file__, clean_dirs=['packages']) + shell_exec("mama clean") + + # https://github.com/BatteredBunny/MamaExampleRemote repo has different commits that either do or dont have the REMOTE_VERSION line + assert not remote_file_contains('ExampleRemote', 'REMOTE_VERSION'), "Tag pinning went wrong" + assert remote_file_contains('ExampleRemote2', 'REMOTE_VERSION 2'), "Tag pinning went wrong" + assert not remote_file_contains('ExampleRemote3', 'REMOTE_VERSION'), "Commit pinning went wrong" + assert remote_file_contains('ExampleRemote4', 'REMOTE_VERSION 2'), "Commit pinning went wrong" diff --git a/tests/test_papa_deploy/CMakeLists.txt b/tests/test_papa_deploy/CMakeLists.txt new file mode 100644 index 0000000..dd54da3 --- /dev/null +++ b/tests/test_papa_deploy/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.6) +project(example_consumer) + +include(mama.cmake) +include_directories(${MAMA_INCLUDES}) + +file(GLOB EXAMPLE_CONSUMER_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp ${CMAKE_CURRENT_SOURCE_DIR}/*.h) +source_group(ExampleConsumer FILES ${EXAMPLE_CONSUMER_SOURCES}) +add_executable(ExampleConsumer ${EXAMPLE_CONSUMER_SOURCES}) +target_link_libraries(ExampleConsumer ${MAMA_LIBS}) + +install(TARGETS ExampleConsumer DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/bin) diff --git a/tests/test_papa_deploy/consumer.cpp b/tests/test_papa_deploy/consumer.cpp new file mode 100644 index 0000000..81d5c19 --- /dev/null +++ b/tests/test_papa_deploy/consumer.cpp @@ -0,0 +1,7 @@ +#include + +int main(int argc, char** argv) +{ + example::print_remote(argv[0]); + return 0; +} \ No newline at end of file diff --git a/tests/test_papa_deploy/mamafile.py b/tests/test_papa_deploy/mamafile.py new file mode 100644 index 0000000..913956b --- /dev/null +++ b/tests/test_papa_deploy/mamafile.py @@ -0,0 +1,7 @@ +import mama + +class ExampleConsumer(mama.BuildTarget): + workspace = 'packages' + + def dependencies(self): + self.add_git('ExampleRemote', 'https://github.com/BatteredBunny/MamaExampleRemote.git', git_tag='4acd9052f27a459314651dd485ae8fa79a04d49d') diff --git a/tests/test_papa_deploy/test_papa_deploy.py b/tests/test_papa_deploy/test_papa_deploy.py new file mode 100644 index 0000000..ebe6634 --- /dev/null +++ b/tests/test_papa_deploy/test_papa_deploy.py @@ -0,0 +1,11 @@ +from testutils import init, shell_exec, file_exists, executable_extension, native_platform_name + +# Generic test to verify basic build and deploy functions work +def test_papa_deploy(): + init(__file__, clean_dirs=['bin', 'packages']) + + shell_exec("mama build") + shell_exec("mama deploy") + + assert file_exists(f'bin/ExampleConsumer{executable_extension()}'), "Deployed executable not found" + assert file_exists(f'packages/ExampleConsumer/{native_platform_name()}/deploy/ExampleConsumer/papa.txt'), "Deployed papa.txt not found for dependency" diff --git a/tests/test_papa_parse/papa.txt b/tests/test_papa_parse/papa.txt new file mode 100644 index 0000000..3596fbb --- /dev/null +++ b/tests/test_papa_parse/papa.txt @@ -0,0 +1,5 @@ +P ExampleConsumer +D git ExampleRemote,https://github.com/BatteredBunny/MamaExampleRemote.git,,,, +I include +I include/test_papa_deploy +L RelWithDebInfo\ExampleConsumer.lib \ No newline at end of file diff --git a/tests/test_papa_parse/test_papa_parse.py b/tests/test_papa_parse/test_papa_parse.py new file mode 100644 index 0000000..a72319d --- /dev/null +++ b/tests/test_papa_parse/test_papa_parse.py @@ -0,0 +1,29 @@ +from testutils import init +from mama.papa_deploy import PapaFileInfo + +# Test papa file format parsing +def test_papa_parse(): + init(__file__) + + papa = PapaFileInfo('papa.txt') + + assert papa.project_name == 'ExampleConsumer' + + assert len(papa.dependencies) == 1 + dep = papa.dependencies[0] + assert dep.is_git + assert dep.name == 'ExampleRemote' + assert dep.url == 'https://github.com/BatteredBunny/MamaExampleRemote.git' + assert dep.branch == '' + assert dep.tag == '' + assert dep.mamafile == '' + + assert len(papa.includes) == 2 + assert papa.includes[0].endswith('include') + assert papa.includes[1].endswith('include/test_papa_deploy') + + assert len(papa.libs) == 1 + assert papa.libs[0].endswith('RelWithDebInfo/ExampleConsumer.lib') + + assert len(papa.syslibs) == 0 + assert len(papa.assets) == 0 diff --git a/tests/test_stale_dep/mamafile.py b/tests/test_stale_dep/mamafile.py new file mode 100644 index 0000000..286c74d --- /dev/null +++ b/tests/test_stale_dep/mamafile.py @@ -0,0 +1,10 @@ +import mama + +class test(mama.BuildTarget): + workspace = 'packages' + + def build(self): + self.nothing_to_build() + + def dependencies(self): + self.add_git('ExampleRemote', 'https://github.com/BatteredBunny/MamaExampleRemote.git') diff --git a/tests/test_stale_dep/test_stale_dep.py b/tests/test_stale_dep/test_stale_dep.py new file mode 100644 index 0000000..7af384c --- /dev/null +++ b/tests/test_stale_dep/test_stale_dep.py @@ -0,0 +1,35 @@ +from testutils import init, shell_exec, file_contains, native_platform_name +from mama.types.git import Git + +REPO_URL = 'https://github.com/BatteredBunny/MamaExampleRemote.git' +OLD_COMMIT = '4acd9052f27a459314651dd485ae8fa79a04d49d' +OLD_SHORT = OLD_COMMIT[:7] + +def get_dep_path(dep_name): + return f'packages/{dep_name}/{dep_name}' + +def get_git_status_path(dep_name): + return f'packages/{dep_name}/{native_platform_name()}/git_status' + +def switch_to_stale_commit(dep_name): + dep_dir = get_dep_path(dep_name) + shell_exec(f'cd {dep_dir} && git reset --hard {OLD_COMMIT}') + + status_file = get_git_status_path(dep_name) + with open(status_file, 'w') as f: + f.write(Git.format_git_status(REPO_URL, '', '', OLD_SHORT)) + +# Simulates stale dependency updating +def test_stale_dep(): + init(__file__, clean_dirs=['packages']) + + dep_dir = get_dep_path('ExampleRemote') + header = f'{dep_dir}/remote.h' + shell_exec('mama build unshallow') + assert file_contains(header, 'REMOTE_VERSION'), 'Failed to clone dependency repo' + + switch_to_stale_commit('ExampleRemote') + assert not file_contains(header, 'REMOTE_VERSION'), 'Failed to switch to stale commit' + + shell_exec('mama update') + assert file_contains(header, 'REMOTE_VERSION'), "Failed updating to latest commit" diff --git a/tests/testutils.py b/tests/testutils.py new file mode 100644 index 0000000..d52e88e --- /dev/null +++ b/tests/testutils.py @@ -0,0 +1,83 @@ +import os +import shutil +import subprocess +import sys +from typing import Iterable, Optional + +import pytest + +def init(caller_file: str = '', clean_dirs: Optional[Iterable[str]] = None): + # Needed for mama commands to perform work in the correct directory + if caller_file: + os.chdir(os.path.dirname(os.path.abspath(caller_file))) + + if clean_dirs is None: + clean_dirs = () + + for d in clean_dirs: + rmdir(d) + +def shell_exec(cmd: str, exit_on_fail: bool = True, echo: bool = True) -> int: + if echo: print(f'exec: {cmd}') + result = subprocess.run(cmd, shell=True) + if result.returncode != 0 and exit_on_fail: + pytest.fail(f'exec failed: code: {result.returncode} {cmd}') + return result.returncode + +def file_contains(filepath: str, text: str) -> bool: + with open(filepath, 'r') as f: + content = f.read() + return text in content + +def file_exists(filepath: str) -> bool: + return os.path.isfile(filepath) + +def is_windows() -> bool: + return os.name == 'nt' + +def is_linux() -> bool: + return os.name == 'posix' and sys.platform != 'darwin' + +def is_macos() -> bool: + return sys.platform == 'darwin' + +def executable_extension() -> str: + if is_windows(): + return '.exe' + + return '' + +def static_library_extension() -> str: + if is_windows(): + return '.lib' + else: + return '.a' + +def dynamic_library_extension() -> str: + if is_windows(): + return '.dll' + elif is_macos(): + return '.dylib' + else: + return '.so' + +# Excludes for example android +def native_platform_name() -> str: + if is_windows(): + return 'windows' + elif is_linux(): + return 'linux' + elif is_macos(): + return 'macos' + else: + raise Exception("Unsupported platform") + +def onerror(func, path, _): + import stat + if not os.access(path, os.W_OK): + os.chmod(path, stat.S_IWUSR) + func(path) + +def rmdir(path: str): + if os.path.exists(path): + shutil.rmtree(path, onerror=onerror) \ No newline at end of file