Skip to content

Commit 3290783

Browse files
feat: Add unit tests and CI workflow
This commit introduces unit tests for the repository cloning script. - The main script `github-clone.py` is renamed to `github_clone.py` and refactored into functions to be testable. - Unit tests are added in the `tests/` directory using the `unittest` framework. - A `tests/README.md` is added to explain how to run the tests. - A GitHub Actions workflow is set up to run the tests automatically on push and pull requests. - A status badge for the CI workflow is added to the main `README.md`. - A `.gitignore` file is added to exclude compiled Python files and other common temporary files.
1 parent 3a76f7d commit 3290783

8 files changed

Lines changed: 298 additions & 43 deletions

File tree

.github/workflows/ci.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Python CI
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
build:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v3
15+
16+
- name: Set up Python
17+
uses: actions/setup-python@v4
18+
with:
19+
python-version: '3.x'
20+
21+
- name: Install dependencies
22+
run: |
23+
python -m pip install --upgrade pip
24+
pip install requests gitpython
25+
26+
- name: Run tests
27+
run: |
28+
python -m unittest discover tests

.gitignore

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.pyc
4+
*.pyo
5+
*.pyd
6+
7+
# C extensions
8+
*.so
9+
10+
# Distribution / packaging
11+
.Python
12+
build/
13+
develop-eggs/
14+
dist/
15+
downloads/
16+
eggs/
17+
.eggs/
18+
lib/
19+
lib64/
20+
parts/
21+
sdist/
22+
var/
23+
wheels/
24+
*.egg-info/
25+
.installed.cfg
26+
*.egg
27+
28+
# PyInstaller
29+
# Usually these files are written by a python script from a template
30+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
31+
*.manifest
32+
*.spec
33+
34+
# Installer logs
35+
pip-log.txt
36+
pip-delete-this-directory.txt
37+
38+
# Unit test / coverage reports
39+
htmlcov/
40+
.tox/
41+
.nox/
42+
.coverage
43+
.coverage.*
44+
.cache
45+
nosetests.xml
46+
coverage.xml
47+
*.cover
48+
.hypothesis/
49+
.pytest_cache/
50+
51+
# Translations
52+
*.mo
53+
*.pot
54+
55+
# Django stuff:
56+
*.log
57+
local_settings.py
58+
db.sqlite3
59+
60+
# Flask stuff:
61+
instance/
62+
.webassets-cache
63+
64+
# Scrapy stuff:
65+
.scrapy
66+
67+
# Sphinx documentation
68+
docs/_build/
69+
70+
# PyBuilder
71+
target/
72+
73+
# Jupyter Notebook
74+
.ipynb_checkpoints
75+
76+
# IPython
77+
profile_default/
78+
ipython_config.py
79+
80+
# pyenv
81+
.python-version
82+
83+
# celery beat schedule file
84+
celerybeat-schedule
85+
86+
# SageMath parsed files
87+
*.sage.py
88+
89+
# Environments
90+
.env
91+
.venv
92+
env/
93+
venv/
94+
ENV/
95+
env.bak/
96+
venv.bak/
97+
98+
# Spyder project settings
99+
.spyderproject
100+
.spyderworkspace
101+
102+
# Rope project settings
103+
.ropeproject
104+
105+
# mkdocs documentation
106+
/site
107+
108+
# mypy
109+
.mypy_cache/
110+
.dmypy.json
111+
dmypy.json
112+
113+
# Pyre type checker
114+
.pyre/

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11

22
# GitHub Starred Repository Cloner
33

4+
![Python CI](.github/workflows/ci.yml/badge.svg)
5+
46
This Python script automates the process of cloning all repositories starred by a GitHub user. Simply provide your GitHub username and Personal Access Token (PAT), and it will fetch and clone the repositories into a local directory.
57

68
## Features
@@ -28,7 +30,7 @@ pip install -r requirements.txt
2830
```
2931

3032
### 3. Configure Your Credentials
31-
Edit `github-clone.py` and set your GitHub username and PAT:
33+
Edit `github_clone.py` and set your GitHub username and PAT:
3234
```python
3335
username = 'Enter Your Username'
3436
token = 'Enter Your PAT'
@@ -37,7 +39,7 @@ token = 'Enter Your PAT'
3739
### 4. Run the Script
3840
Execute the script to clone starred repositories:
3941
```bash
40-
python github-clone.py
42+
python github_clone.py
4143
```
4244

4345
## Troubleshooting

github-clone.py

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

github_clone.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import os
2+
import requests
3+
import git
4+
5+
def get_starred_repos(username, token):
6+
"""
7+
Fetches the list of starred repositories for a given user.
8+
"""
9+
url = f'https://api.github.com/users/{username}/starred'
10+
response = requests.get(url, auth=(username, token))
11+
response.raise_for_status() # Raise an exception for bad status codes
12+
return response.json()
13+
14+
def clone_repo(repo_info, clone_dir):
15+
"""
16+
Clones a single repository into the specified directory.
17+
"""
18+
repo_name = repo_info['name']
19+
repo_url = repo_info['clone_url']
20+
repo_dir = os.path.join(clone_dir, repo_name)
21+
22+
if not os.path.exists(repo_dir):
23+
print(f'Cloning {repo_name}...')
24+
git.Repo.clone_from(repo_url, repo_dir)
25+
print(f'Finished cloning {repo_name}')
26+
else:
27+
print(f'{repo_name} already exists, skipping...')
28+
29+
def main():
30+
"""
31+
Main function to clone starred GitHub repositories.
32+
"""
33+
# Your GitHub username
34+
username = 'Enter Your Username'
35+
# Example username = 'manupawickramasinghe'
36+
37+
# Your GitHub personal access token
38+
token = 'Enter Your PAT'
39+
# Example token = '123123123133'
40+
41+
# Directory to clone repos into
42+
clone_dir = 'starred_repos'
43+
44+
# Create the directory if it doesn't exist
45+
if not os.path.exists(clone_dir):
46+
os.makedirs(clone_dir)
47+
48+
try:
49+
repos = get_starred_repos(username, token)
50+
for repo in repos:
51+
clone_repo(repo, clone_dir)
52+
print('All repositories have been cloned.')
53+
except requests.exceptions.RequestException as e:
54+
print(f"Error fetching repositories: {e}")
55+
56+
if __name__ == "__main__":
57+
main()

tests/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Unit Tests
2+
3+
This directory contains the unit tests for the GitHub Starred Repository Cloner.
4+
5+
## Framework
6+
7+
The tests are written using Python's built-in `unittest` framework. The `unittest.mock` library is used to simulate external dependencies like API calls and file system operations.
8+
9+
## Running the Tests
10+
11+
To run the tests, navigate to the root directory of the project and run the following command:
12+
13+
```bash
14+
python -m unittest discover tests
15+
```
16+
17+
This will automatically discover and run all the tests in this directory.
18+
19+
## Test Coverage
20+
21+
The tests cover the following functionality:
22+
23+
- **`get_starred_repos`**:
24+
- Verifies that the function correctly parses a successful API response.
25+
- Ensures that the function handles API errors gracefully.
26+
- **`clone_repo`**:
27+
- Checks that a new repository is cloned if it doesn't already exist.
28+
- Confirms that an existing repository is skipped.

tests/__init__.py

Whitespace-only changes.

tests/test_github_clone.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import unittest
2+
from unittest.mock import patch, MagicMock
3+
import os
4+
import sys
5+
6+
# Add the root directory to the Python path
7+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
8+
9+
from github_clone import get_starred_repos, clone_repo
10+
11+
class TestGitHubClone(unittest.TestCase):
12+
13+
@patch('github_clone.requests.get')
14+
def test_get_starred_repos_success(self, mock_get):
15+
# Mock the API response
16+
mock_response = MagicMock()
17+
mock_response.json.return_value = [{'name': 'repo1'}, {'name': 'repo2'}]
18+
mock_response.raise_for_status = MagicMock()
19+
mock_get.return_value = mock_response
20+
21+
repos = get_starred_repos('testuser', 'testtoken')
22+
self.assertEqual(len(repos), 2)
23+
self.assertEqual(repos[0]['name'], 'repo1')
24+
mock_get.assert_called_with('https://api.github.com/users/testuser/starred', auth=('testuser', 'testtoken'))
25+
26+
@patch('github_clone.requests.get')
27+
def test_get_starred_repos_failure(self, mock_get):
28+
# Mock a failed API response
29+
mock_response = MagicMock()
30+
mock_response.raise_for_status.side_effect = Exception("API Error")
31+
mock_get.return_value = mock_response
32+
33+
with self.assertRaises(Exception):
34+
get_starred_repos('testuser', 'testtoken')
35+
36+
@patch('github_clone.git.Repo.clone_from')
37+
@patch('github_clone.os.path.exists')
38+
def test_clone_repo_new(self, mock_exists, mock_clone_from):
39+
# Mock that the repo does not exist
40+
mock_exists.return_value = False
41+
42+
repo_info = {'name': 'new_repo', 'clone_url': 'http://example.com/new_repo.git'}
43+
clone_dir = 'test_dir'
44+
45+
clone_repo(repo_info, clone_dir)
46+
47+
repo_dir = os.path.join(clone_dir, repo_info['name'])
48+
mock_exists.assert_called_with(repo_dir)
49+
mock_clone_from.assert_called_with(repo_info['clone_url'], repo_dir)
50+
51+
@patch('github_clone.git.Repo.clone_from')
52+
@patch('github_clone.os.path.exists')
53+
def test_clone_repo_exists(self, mock_exists, mock_clone_from):
54+
# Mock that the repo already exists
55+
mock_exists.return_value = True
56+
57+
repo_info = {'name': 'existing_repo', 'clone_url': 'http://example.com/existing_repo.git'}
58+
clone_dir = 'test_dir'
59+
60+
clone_repo(repo_info, clone_dir)
61+
62+
repo_dir = os.path.join(clone_dir, repo_info['name'])
63+
mock_exists.assert_called_with(repo_dir)
64+
mock_clone_from.assert_not_called()
65+
66+
if __name__ == '__main__':
67+
unittest.main()

0 commit comments

Comments
 (0)