diff --git a/cms/djangoapps/github_sync/__init__.py b/cms/djangoapps/github_sync/__init__.py index d00949e56e..c901ad37e7 100644 --- a/cms/djangoapps/github_sync/__init__.py +++ b/cms/djangoapps/github_sync/__init__.py @@ -1,10 +1,16 @@ -from git import Repo -from contentstore import import_from_xml -from fs.osfs import OSFS +import logging import os -from xmodule.modulestore import Location -from django.conf import settings +from django.conf import settings +from fs.osfs import OSFS +from git import Repo, PushInfo + +from contentstore import import_from_xml +from xmodule.modulestore import Location + +from .exceptions import GithubSyncError + +log = logging.getLogger(__name__) def import_from_github(repo_settings): """ @@ -53,4 +59,17 @@ def export_to_github(course, repo_path, commit_message): origin = git_repo.remotes.origin if settings.MITX_FEATURES['GITHUB_PUSH']: - origin.push() + push_infos = origin.push() + if len(push_infos) > 1: + log.error('Unexpectedly pushed multiple heads: {infos}'.format( + infos="\n".join(str(info.summary) for info in push_infos) + )) + + if push_infos[0].flags & PushInfo.ERROR: + log.error('Failed push: flags={p.flags}, local_ref={p.local_ref}, ' + 'remote_ref_string={p.remote_ref_string}, ' + 'remote_ref={p.remote_ref}, old_commit={p.old_commit}, ' + 'summary={p.summary})'.format(p=push_infos[0])) + raise GithubSyncError('Failed to push: {info}'.format( + info=str(push_infos[0].summary) + )) diff --git a/cms/djangoapps/github_sync/exceptions.py b/cms/djangoapps/github_sync/exceptions.py new file mode 100644 index 0000000000..9097ffc2a6 --- /dev/null +++ b/cms/djangoapps/github_sync/exceptions.py @@ -0,0 +1,2 @@ +class GithubSyncError(Exception): + pass diff --git a/cms/djangoapps/github_sync/tests/__init__.py b/cms/djangoapps/github_sync/tests/__init__.py new file mode 100644 index 0000000000..825fc68313 --- /dev/null +++ b/cms/djangoapps/github_sync/tests/__init__.py @@ -0,0 +1,96 @@ +from django.test import TestCase +from path import path +import shutil +from github_sync import import_from_github, export_to_github, repo_path_from_location +from git import Repo +from django.conf import settings +from xmodule.modulestore.django import modulestore +from xmodule.modulestore import Location +from override_settings import override_settings +from github_sync.exceptions import GithubSyncError + + +class GithubSyncTestCase(TestCase): + + def setUp(self): + self.working_dir = path(settings.TEST_ROOT) + self.repo_dir = self.working_dir / 'local_repo' + self.remote_dir = self.working_dir / 'remote_repo' + shutil.copytree('common/test/data/toy', self.remote_dir) + + remote = Repo.init(self.remote_dir) + remote.git.add(A=True) + remote.git.commit(m='Initial commit') + remote.git.config("receive.denyCurrentBranch", "ignore") + + modulestore().collection.drop() + + self.import_revision, self.import_course = import_from_github({ + 'path': self.repo_dir, + 'origin': self.remote_dir, + 'branch': 'master', + 'org': 'org', + 'course': 'course' + }) + + def tearDown(self): + shutil.rmtree(self.repo_dir) + shutil.rmtree(self.remote_dir) + + def test_initialize_repo(self): + """ + Test that importing from github will create a repo if the repo doesn't already exist + """ + self.assertEquals(1, len(Repo(self.repo_dir).head.reference.log())) + + def test_import_contents(self): + """ + Test that the import loads the correct course into the modulestore + """ + self.assertEquals('Toy Course', self.import_course.metadata['display_name']) + self.assertIn( + Location('i4x://org/course/chapter/Overview'), + [child.location for child in self.import_course.get_children()]) + self.assertEquals(1, len(self.import_course.get_children())) + + @override_settings(MITX_FEATURES={'GITHUB_PUSH': False}) + def test_export_no_pash(self): + """ + Test that with the GITHUB_PUSH feature disabled, no content is pushed to the remote + """ + export_to_github(self.import_course, self.repo_dir, 'Test no-push') + self.assertEquals(1, Repo(self.remote_dir).head.commit.count()) + + @override_settings(MITX_FEATURES={'GITHUB_PUSH': True}) + def test_export_push(self): + """ + Test that with GITHUB_PUSH enabled, content is pushed to the remote + """ + self.import_course.metadata['display_name'] = 'Changed display name' + export_to_github(self.import_course, self.repo_dir, 'Test push') + self.assertEquals(2, Repo(self.remote_dir).head.commit.count()) + + @override_settings(MITX_FEATURES={'GITHUB_PUSH': True}) + def test_export_conflict(self): + """ + Test that if there is a conflict when pushing to the remote repo, nothing is pushed and an exception is raised + """ + self.import_course.metadata['display_name'] = 'Changed display name' + + remote = Repo(self.remote_dir) + remote.git.commit(allow_empty=True, m="Testing conflict commit") + + self.assertRaises(GithubSyncError, export_to_github, self.import_course, self.repo_dir, 'Test push') + self.assertEquals(2, remote.head.reference.commit.count()) + self.assertEquals("Testing conflict commit\n", remote.head.reference.commit.message) + + +@override_settings(REPOS={'namea': {'path': 'patha', 'org': 'orga', 'course': 'coursea'}, + 'nameb': {'path': 'pathb', 'org': 'orgb', 'course': 'courseb'}}) +class RepoPathLookupTestCase(TestCase): + def test_successful_lookup(self): + self.assertEquals('patha', repo_path_from_location('i4x://orga/coursea/course/foo')) + self.assertEquals('pathb', repo_path_from_location('i4x://orgb/courseb/course/foo')) + + def test_failed_lookup(self): + self.assertEquals(None, repo_path_from_location('i4x://c/c/course/foo')) diff --git a/cms/envs/test.py b/cms/envs/test.py index 032de92953..927e2af987 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -17,10 +17,18 @@ for app in os.listdir(PROJECT_ROOT / 'djangoapps'): NOSE_ARGS += ['--cover-package', app] TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' +TEST_ROOT = 'test_root' + MODULESTORE = { - 'host': 'localhost', - 'db': 'mongo_base', - 'collection': 'key_store', + 'default': { + 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', + 'OPTIONS': { + 'default_class': 'xmodule.raw_module.RawDescriptor', + 'host': 'localhost', + 'db': 'test_xmodule', + 'collection': 'modulestore', + } + } } DATABASES = { diff --git a/common/test/data/toy/toy_course.xml b/common/test/data/toy/course.xml similarity index 100% rename from common/test/data/toy/toy_course.xml rename to common/test/data/toy/course.xml diff --git a/requirements.txt b/requirements.txt index ddf218af7c..571c530a2f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,3 +29,4 @@ django_nose nosexcover rednose GitPython >= 0.3 +django-override-settings diff --git a/test_root/.gitignore b/test_root/.gitignore new file mode 100644 index 0000000000..b3e5512f73 --- /dev/null +++ b/test_root/.gitignore @@ -0,0 +1,2 @@ +local_repo +remote_repo