diff --git a/.github/workflows/bats.yml b/.github/workflows/bats.yml deleted file mode 100644 index e88e262..0000000 --- a/.github/workflows/bats.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Bats -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - unit-test: - runs-on: ubuntu-latest - name: Bats - steps: - - uses: actions/checkout@v2 - - - name: install bats - run: | - git clone --depth=1 https://github.com/bats-core/bats-core.git - - - name: run tests - run: bats-core/bin/bats ./tests.bat diff --git a/.github/workflows/test-hooks.yml b/.github/workflows/test-hooks.yml new file mode 100644 index 0000000..ed5df8d --- /dev/null +++ b/.github/workflows/test-hooks.yml @@ -0,0 +1,49 @@ +name: Test Hooks +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + test: + name: Test on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + # Windows-specific: Install git via Scoop mingit + - name: Install Scoop and mingit (Windows) + if: runner.os == 'Windows' + shell: powershell + run: | + # Install Scoop if not already installed + if (-not (Get-Command scoop -ErrorAction SilentlyContinue)) { + Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force + Invoke-RestMethod get.scoop.sh | Invoke-Expression + } + + # Install mingit via Scoop + scoop install mingit + + # Verify git installation + git --version + + # macOS and Linux: Ensure git is available + - name: Verify git installation (Unix) + if: runner.os != 'Windows' + run: git --version + + - name: Run Python tests + run: python test_hooks.py diff --git a/README.md b/README.md index 096eb87..de095a8 100644 --- a/README.md +++ b/README.md @@ -43,3 +43,15 @@ Otherwise you need set full path relative `.git`. Following example tells the sc ``` git config unity3d.assets-dir client/Assets ``` + +## Testing + +This project includes automated tests that work on Linux, macOS, and Windows. + +To run tests locally: + +```bash +python3 test_hooks.py +``` + +The tests are automatically run on GitHub Actions for all three platforms. diff --git a/test_hooks.py b/test_hooks.py new file mode 100755 index 0000000..efd98fe --- /dev/null +++ b/test_hooks.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +""" +Test suite for Unity Git Hooks +Replaces BATS-based tests with Python-based tests that work on macOS and Windows +""" + +import os +import platform +import shutil +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +class GitHooksTestCase(unittest.TestCase): + """Base test case with setup and teardown for git repository""" + + def setUp(self): + """Create a temporary git repository for testing""" + # Create temporary directory + self.test_dir = tempfile.mkdtemp(prefix='unity-git-hooks-test-') + self.repo_dir = os.path.join(self.test_dir, 'repo') + os.makedirs(self.repo_dir) + + # Initialize git repository + self._run_git(['init']) + self._run_git(['config', 'user.email', 'test@ci']) + self._run_git(['config', 'user.name', 'test']) + + # Create Assets directory + self.assets_dir = os.path.join(self.repo_dir, 'Assets') + os.makedirs(self.assets_dir) + + # Install git hooks so they run automatically + self._install_hooks() + + def tearDown(self): + """Clean up temporary directory""" + if os.path.exists(self.test_dir): + # On Windows, git may keep file handles open, so we need to handle errors + def handle_remove_readonly(func, path, exc): + """Error handler for Windows readonly files""" + import stat + if not os.access(path, os.W_OK): + # Make the file writable and try again + os.chmod(path, stat.S_IWUSR) + func(path) + else: + raise + + shutil.rmtree(self.test_dir, onerror=handle_remove_readonly) + + def _run_git(self, args, check=True, capture_output=False): + """ + Run git command in the test repository + + On Windows with Scoop mingit, use PowerShell to invoke git + """ + if platform.system() == 'Windows': + # Check if git is available via Scoop mingit + git_cmd = self._find_git_windows() + cmd = [git_cmd] + args + else: + cmd = ['git'] + args + + result = subprocess.run( + cmd, + cwd=self.repo_dir, + check=check, + capture_output=capture_output, + text=True + ) + + if capture_output: + return result + return result.returncode + + def _find_git_windows(self): + """ + Find git executable on Windows + Prioritize Scoop mingit installation + """ + # Try Scoop mingit first + scoop_git = os.path.expandvars(r'%USERPROFILE%\scoop\apps\mingit\current\cmd\git.exe') + if os.path.exists(scoop_git): + return scoop_git + + # Try Scoop shims directory + scoop_shim = os.path.expandvars(r'%USERPROFILE%\scoop\shims\git.exe') + if os.path.exists(scoop_shim): + return scoop_shim + + # Fallback to system git + return shutil.which('git') or 'git' + + def _install_hooks(self): + """ + Install git hooks into the test repository + Hooks will be run automatically by git + """ + # Get the scripts directory + script_dir = Path(__file__).parent / 'scripts' + + # Create hooks directory if it doesn't exist + hooks_dir = os.path.join(self.repo_dir, '.git', 'hooks') + os.makedirs(hooks_dir, exist_ok=True) + + # Copy hook files + hooks = ['pre-commit', 'post-checkout', 'post-merge'] + for hook in hooks: + src = script_dir / hook + dst = os.path.join(hooks_dir, hook) + if src.exists(): + shutil.copy2(str(src), dst) + # Make executable on Unix systems + if platform.system() != 'Windows': + os.chmod(dst, 0o755) + + +class TestPreCommitHook(GitHooksTestCase): + """Test cases for pre-commit hook""" + + def test_ensuring_meta_is_committed(self): + """Test that missing .meta file causes pre-commit to fail""" + # Create an asset file without .meta + asset_file = os.path.join(self.assets_dir, 'assets') + Path(asset_file).touch() + self._run_git(['add', 'Assets/assets']) + + # Try to commit - pre-commit hook should fail automatically + result = self._run_git(['commit', '-m', 'test commit'], check=False, capture_output=True) + self.assertNotEqual(result.returncode, 0) + self.assertIn('Error: Missing meta file', result.stderr) + + # Now add the .meta file + meta_file = os.path.join(self.assets_dir, 'assets.meta') + Path(meta_file).touch() + self._run_git(['add', 'Assets/assets.meta']) + + # Try to commit again - should succeed + result = self._run_git(['commit', '-m', 'test commit'], check=False, capture_output=True) + self.assertEqual(result.returncode, 0) + + def test_ignoring_assets_file_starting_with_dot(self): + """Test that asset files starting with dot are ignored""" + # Create a hidden asset file (no .meta needed) + hidden_file = os.path.join(self.assets_dir, '.assets') + Path(hidden_file).touch() + self._run_git(['add', '--force', 'Assets/.assets']) + + # Commit should succeed via pre-commit hook + result = self._run_git(['commit', '-m', 'test commit'], check=False, capture_output=True) + self.assertEqual(result.returncode, 0) + + def test_renaming_directory(self): + """Test that renaming a directory requires updating .meta files""" + # Create directory with .gitkeep and .meta + dir_path = os.path.join(self.assets_dir, 'dir') + os.makedirs(dir_path) + + gitkeep_file = os.path.join(dir_path, '.gitkeep') + Path(gitkeep_file).touch() + + meta_file = os.path.join(self.assets_dir, 'dir.meta') + Path(meta_file).touch() + + self._run_git(['add', '--force', '--all']) + self._run_git(['commit', '-m', 'add Assets/dir']) + + # Rename the directory + new_dir_path = os.path.join(self.assets_dir, 'dir-new') + shutil.move(dir_path, new_dir_path) + self._run_git(['add', '--force', '--all']) + + # Try to commit - pre-commit hook should fail because old .meta still exists + result = self._run_git(['commit', '-m', 'rename dir'], check=False, capture_output=True) + self.assertNotEqual(result.returncode, 0) + self.assertIn('Error: Redudant meta file', result.stderr) + + +def run_tests(): + """Run all tests""" + # Discover and run tests + loader = unittest.TestLoader() + suite = loader.loadTestsFromModule(sys.modules[__name__]) + runner = unittest.TextTestRunner(verbosity=2) + result = runner.run(suite) + + # Return exit code based on test results + return 0 if result.wasSuccessful() else 1 + + +if __name__ == '__main__': + sys.exit(run_tests()) diff --git a/tests.bat b/tests.bat deleted file mode 100755 index 7213454..0000000 --- a/tests.bat +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env bats - -PRE_COMMIT="$BATS_TEST_DIRNAME/scripts/pre-commit" - -setup() { - git init repo >&2 - cd repo - git config user.email "bats@ci" - git config user.name "bats" - mkdir Assets -} - -teardown() { - cd .. - rm -rf repo -} - -@test "ensuring .meta is commit" { - touch Assets/assets - git add Assets/assets - run "$PRE_COMMIT" - echo "$output" - [ "$status" -eq 1 ] - [ "${lines[0]}" = "Error: Missing meta file." ] - - touch Assets/assets.meta - git add Assets/assets.meta - "$PRE_COMMIT" -} - -@test "ignoring assets file which starts with dot" { - touch Assets/.assets - git add --force Assets/.assets - "$PRE_COMMIT" -} - -@test "renaming directory" { - mkdir Assets/dir - touch Assets/dir/.gitkeep - touch Assets/dir.meta - git add --force --all - git commit -n -m 'add Assets/dir' - mv Assets/dir Assets/dir-new - git add --force --all - run "$PRE_COMMIT" - echo "$output" - [ "$status" -eq 1 ] - [ "${lines[0]}" = "Error: Redudant meta file." ] -}