Skip to content

Commit 3e91c88

Browse files
Copilotdoitian
andauthored
Rewrite tests in Python with cross-platform support (#14)
* Initial plan * Add Python-based tests with multi-platform support Co-authored-by: doitian <35768+doitian@users.noreply.github.com> * Update README with testing information Co-authored-by: doitian <35768+doitian@users.noreply.github.com> * Make git run hooks automatically and delete TESTING.md Co-authored-by: doitian <35768+doitian@users.noreply.github.com> * Remove useless chmod step and delete bats workflow Co-authored-by: doitian <35768+doitian@users.noreply.github.com> * Fix Windows CI failure and delete tests.bat Co-authored-by: doitian <35768+doitian@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: doitian <35768+doitian@users.noreply.github.com>
1 parent 94cf649 commit 3e91c88

File tree

5 files changed

+257
-69
lines changed

5 files changed

+257
-69
lines changed

.github/workflows/bats.yml

Lines changed: 0 additions & 20 deletions
This file was deleted.

.github/workflows/test-hooks.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Test Hooks
2+
on:
3+
push:
4+
branches: [ master ]
5+
pull_request:
6+
branches: [ master ]
7+
8+
jobs:
9+
test:
10+
name: Test on ${{ matrix.os }}
11+
runs-on: ${{ matrix.os }}
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
os: [ubuntu-latest, macos-latest, windows-latest]
16+
17+
steps:
18+
- name: Checkout repository
19+
uses: actions/checkout@v3
20+
21+
- name: Set up Python
22+
uses: actions/setup-python@v4
23+
with:
24+
python-version: '3.x'
25+
26+
# Windows-specific: Install git via Scoop mingit
27+
- name: Install Scoop and mingit (Windows)
28+
if: runner.os == 'Windows'
29+
shell: powershell
30+
run: |
31+
# Install Scoop if not already installed
32+
if (-not (Get-Command scoop -ErrorAction SilentlyContinue)) {
33+
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force
34+
Invoke-RestMethod get.scoop.sh | Invoke-Expression
35+
}
36+
37+
# Install mingit via Scoop
38+
scoop install mingit
39+
40+
# Verify git installation
41+
git --version
42+
43+
# macOS and Linux: Ensure git is available
44+
- name: Verify git installation (Unix)
45+
if: runner.os != 'Windows'
46+
run: git --version
47+
48+
- name: Run Python tests
49+
run: python test_hooks.py

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,15 @@ Otherwise you need set full path relative `.git`. Following example tells the sc
4343
```
4444
git config unity3d.assets-dir client/Assets
4545
```
46+
47+
## Testing
48+
49+
This project includes automated tests that work on Linux, macOS, and Windows.
50+
51+
To run tests locally:
52+
53+
```bash
54+
python3 test_hooks.py
55+
```
56+
57+
The tests are automatically run on GitHub Actions for all three platforms.

test_hooks.py

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Test suite for Unity Git Hooks
4+
Replaces BATS-based tests with Python-based tests that work on macOS and Windows
5+
"""
6+
7+
import os
8+
import platform
9+
import shutil
10+
import subprocess
11+
import sys
12+
import tempfile
13+
import unittest
14+
from pathlib import Path
15+
16+
17+
class GitHooksTestCase(unittest.TestCase):
18+
"""Base test case with setup and teardown for git repository"""
19+
20+
def setUp(self):
21+
"""Create a temporary git repository for testing"""
22+
# Create temporary directory
23+
self.test_dir = tempfile.mkdtemp(prefix='unity-git-hooks-test-')
24+
self.repo_dir = os.path.join(self.test_dir, 'repo')
25+
os.makedirs(self.repo_dir)
26+
27+
# Initialize git repository
28+
self._run_git(['init'])
29+
self._run_git(['config', 'user.email', 'test@ci'])
30+
self._run_git(['config', 'user.name', 'test'])
31+
32+
# Create Assets directory
33+
self.assets_dir = os.path.join(self.repo_dir, 'Assets')
34+
os.makedirs(self.assets_dir)
35+
36+
# Install git hooks so they run automatically
37+
self._install_hooks()
38+
39+
def tearDown(self):
40+
"""Clean up temporary directory"""
41+
if os.path.exists(self.test_dir):
42+
# On Windows, git may keep file handles open, so we need to handle errors
43+
def handle_remove_readonly(func, path, exc):
44+
"""Error handler for Windows readonly files"""
45+
import stat
46+
if not os.access(path, os.W_OK):
47+
# Make the file writable and try again
48+
os.chmod(path, stat.S_IWUSR)
49+
func(path)
50+
else:
51+
raise
52+
53+
shutil.rmtree(self.test_dir, onerror=handle_remove_readonly)
54+
55+
def _run_git(self, args, check=True, capture_output=False):
56+
"""
57+
Run git command in the test repository
58+
59+
On Windows with Scoop mingit, use PowerShell to invoke git
60+
"""
61+
if platform.system() == 'Windows':
62+
# Check if git is available via Scoop mingit
63+
git_cmd = self._find_git_windows()
64+
cmd = [git_cmd] + args
65+
else:
66+
cmd = ['git'] + args
67+
68+
result = subprocess.run(
69+
cmd,
70+
cwd=self.repo_dir,
71+
check=check,
72+
capture_output=capture_output,
73+
text=True
74+
)
75+
76+
if capture_output:
77+
return result
78+
return result.returncode
79+
80+
def _find_git_windows(self):
81+
"""
82+
Find git executable on Windows
83+
Prioritize Scoop mingit installation
84+
"""
85+
# Try Scoop mingit first
86+
scoop_git = os.path.expandvars(r'%USERPROFILE%\scoop\apps\mingit\current\cmd\git.exe')
87+
if os.path.exists(scoop_git):
88+
return scoop_git
89+
90+
# Try Scoop shims directory
91+
scoop_shim = os.path.expandvars(r'%USERPROFILE%\scoop\shims\git.exe')
92+
if os.path.exists(scoop_shim):
93+
return scoop_shim
94+
95+
# Fallback to system git
96+
return shutil.which('git') or 'git'
97+
98+
def _install_hooks(self):
99+
"""
100+
Install git hooks into the test repository
101+
Hooks will be run automatically by git
102+
"""
103+
# Get the scripts directory
104+
script_dir = Path(__file__).parent / 'scripts'
105+
106+
# Create hooks directory if it doesn't exist
107+
hooks_dir = os.path.join(self.repo_dir, '.git', 'hooks')
108+
os.makedirs(hooks_dir, exist_ok=True)
109+
110+
# Copy hook files
111+
hooks = ['pre-commit', 'post-checkout', 'post-merge']
112+
for hook in hooks:
113+
src = script_dir / hook
114+
dst = os.path.join(hooks_dir, hook)
115+
if src.exists():
116+
shutil.copy2(str(src), dst)
117+
# Make executable on Unix systems
118+
if platform.system() != 'Windows':
119+
os.chmod(dst, 0o755)
120+
121+
122+
class TestPreCommitHook(GitHooksTestCase):
123+
"""Test cases for pre-commit hook"""
124+
125+
def test_ensuring_meta_is_committed(self):
126+
"""Test that missing .meta file causes pre-commit to fail"""
127+
# Create an asset file without .meta
128+
asset_file = os.path.join(self.assets_dir, 'assets')
129+
Path(asset_file).touch()
130+
self._run_git(['add', 'Assets/assets'])
131+
132+
# Try to commit - pre-commit hook should fail automatically
133+
result = self._run_git(['commit', '-m', 'test commit'], check=False, capture_output=True)
134+
self.assertNotEqual(result.returncode, 0)
135+
self.assertIn('Error: Missing meta file', result.stderr)
136+
137+
# Now add the .meta file
138+
meta_file = os.path.join(self.assets_dir, 'assets.meta')
139+
Path(meta_file).touch()
140+
self._run_git(['add', 'Assets/assets.meta'])
141+
142+
# Try to commit again - should succeed
143+
result = self._run_git(['commit', '-m', 'test commit'], check=False, capture_output=True)
144+
self.assertEqual(result.returncode, 0)
145+
146+
def test_ignoring_assets_file_starting_with_dot(self):
147+
"""Test that asset files starting with dot are ignored"""
148+
# Create a hidden asset file (no .meta needed)
149+
hidden_file = os.path.join(self.assets_dir, '.assets')
150+
Path(hidden_file).touch()
151+
self._run_git(['add', '--force', 'Assets/.assets'])
152+
153+
# Commit should succeed via pre-commit hook
154+
result = self._run_git(['commit', '-m', 'test commit'], check=False, capture_output=True)
155+
self.assertEqual(result.returncode, 0)
156+
157+
def test_renaming_directory(self):
158+
"""Test that renaming a directory requires updating .meta files"""
159+
# Create directory with .gitkeep and .meta
160+
dir_path = os.path.join(self.assets_dir, 'dir')
161+
os.makedirs(dir_path)
162+
163+
gitkeep_file = os.path.join(dir_path, '.gitkeep')
164+
Path(gitkeep_file).touch()
165+
166+
meta_file = os.path.join(self.assets_dir, 'dir.meta')
167+
Path(meta_file).touch()
168+
169+
self._run_git(['add', '--force', '--all'])
170+
self._run_git(['commit', '-m', 'add Assets/dir'])
171+
172+
# Rename the directory
173+
new_dir_path = os.path.join(self.assets_dir, 'dir-new')
174+
shutil.move(dir_path, new_dir_path)
175+
self._run_git(['add', '--force', '--all'])
176+
177+
# Try to commit - pre-commit hook should fail because old .meta still exists
178+
result = self._run_git(['commit', '-m', 'rename dir'], check=False, capture_output=True)
179+
self.assertNotEqual(result.returncode, 0)
180+
self.assertIn('Error: Redudant meta file', result.stderr)
181+
182+
183+
def run_tests():
184+
"""Run all tests"""
185+
# Discover and run tests
186+
loader = unittest.TestLoader()
187+
suite = loader.loadTestsFromModule(sys.modules[__name__])
188+
runner = unittest.TextTestRunner(verbosity=2)
189+
result = runner.run(suite)
190+
191+
# Return exit code based on test results
192+
return 0 if result.wasSuccessful() else 1
193+
194+
195+
if __name__ == '__main__':
196+
sys.exit(run_tests())

tests.bat

Lines changed: 0 additions & 49 deletions
This file was deleted.

0 commit comments

Comments
 (0)