diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fbf27992..c17079a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,8 +26,8 @@ jobs: - name: Python setup uses: actions/setup-python@v4 with: - python-version: ${{ matrix.python-version }} - + python-version: ${{ matrix.python-version }} + - name: tox install run: pip install tox @@ -36,6 +36,15 @@ jobs: TOXENV: ${{ matrix.toxenv }} run: tox + - name: Run Integration Tests + run: | + cd .. + git clone https://github.com/edx/devstack.git + cd devstack + sed -i 's/:cached//g' ./docker-compose-host.yml + make dev.clone.https + DEVSTACK_WORKSPACE=$PWD/.. docker-compose -f docker-compose.yml -f docker-compose-host.yml run -v $PWD/../edx-sga:/edx-sga lms /edx-sga/run_devstack_integration_tests.sh + - name: Upload coverage to CodeCov if: matrix.python-version == '3.8' && matrix.toxenv == 'py38-django32' uses: codecov/codecov-action@v3 diff --git a/edx_sga/sga.py b/edx_sga/sga.py index aecbbe0a..636200a9 100644 --- a/edx_sga/sga.py +++ b/edx_sga/sga.py @@ -656,7 +656,7 @@ def get_student_item_dict(self, student_id=None): """ if student_id is None and (user_service := self.runtime.service(self, 'user')): student_id = user_service.get_current_user().opt_attrs.get(ATTR_KEY_ANONYMOUS_USER_ID) - + assert student_id != ("MOCK", "Forgot to call 'personalize' in test.") return { "student_id": student_id, @@ -762,7 +762,7 @@ def student_state(self): if score: score = score.get("points_earned") graded = {"score": score, "comment": force_str(self.comment)} - + else: uploaded = None diff --git a/edx_sga/tests/integration_tests.py b/edx_sga/tests/integration_tests.py index c85311e2..6d2f7af9 100644 --- a/edx_sga/tests/integration_tests.py +++ b/edx_sga/tests/integration_tests.py @@ -7,7 +7,6 @@ import json import os import shutil -import functools import tempfile from unittest import mock @@ -32,16 +31,13 @@ from submissions.models import StudentItem from xblock.field_data import DictFieldData from xblock.fields import ScopeIds -from xblock.runtime import DictKeyValueStore, KvsFieldData +from xblock.runtime import DictKeyValueStore, KvsFieldData, Mixologist from xblock.test.tools import TestRuntime from xmodule.modulestore.django import modulestore -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, mixed_store_config, StoreConstructors +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory from xmodule.modulestore.xml_exporter import export_course_to_xml from xmodule.modulestore.xml_importer import import_course_from_xml -from xmodule.modulestore.inheritance import InheritanceMixin -from openedx.core.lib import tempdir -from xblock.runtime import Mixologist from edx_sga.constants import ShowAnswer from edx_sga.sga import StaffGradedAssignmentXBlock @@ -166,7 +162,7 @@ def make_student(self, block, name, make_state=True, **state): module.save() anonymous_id = anonymous_id_for_user(user, self.course_id) - + item = StudentItem( student_id=anonymous_id, course_id=self.course_id, @@ -175,14 +171,14 @@ def make_student(self, block, name, make_state=True, **state): ) item.save() - if answer: + if answer: student_id = block.get_student_item_dict(anonymous_id) submission = submissions_api.create_submission(student_id, answer) if score is not None: submissions_api.set_score( submission["uuid"], score, block.max_score() ) - pass + else: submission = None @@ -204,7 +200,7 @@ def personalize(self, block, module, item, submission): state = json.loads(student_module.state) for key, value in state.items(): setattr(block, key, value) - + self.runtime.deprecated_anonymous_student_id = item.student_id def test_ctor(self): @@ -254,7 +250,6 @@ def test_student_view(self, fragment, render_template): self.assertEqual(student_state["upload_allowed"], True) self.assertEqual(student_state["max_score"], 100) self.assertEqual(student_state["graded"], None) - # pylint: disable=no-member fragment.add_css.assert_called_once_with( DummyResource("static/css/edx_sga.css") ) @@ -327,7 +322,6 @@ def test_student_view_with_score(self, fragment, render_template): self.assertEqual(student_state["upload_allowed"], False) self.assertEqual(student_state["max_score"], 100) self.assertEqual(student_state["graded"], {"comment": "", "score": 10}) - # pylint: disable=no-member fragment.add_css.assert_called_once_with( DummyResource("static/css/edx_sga.css") ) @@ -566,7 +560,8 @@ def test_staff_download(self): user = student["module"].student student_id = anonymous_id_for_user(user,self.course_id) - with mock.patch.object(StaffGradedAssignmentXBlock.get_student_item_dict,"__defaults__",(student_id,)), self.dummy_upload(filename, text) as (upload, __): + with mock.patch.object(StaffGradedAssignmentXBlock.get_student_item_dict,"__defaults__",(student_id,)),\ + self.dummy_upload(filename, text) as (upload, __): block.upload_assignment(mock.Mock(params={"assignment": upload})) students.append( ( @@ -627,8 +622,9 @@ def test_staff_download_unicode_filename(self): self.personalize(block, **student) user = student["module"].student student_id = anonymous_id_for_user(user,self.course_id) - - with mock.patch.object(StaffGradedAssignmentXBlock.get_student_item_dict,"__defaults__",(student_id,)), self.dummy_upload("файл.txt") as (upload, expected): + + with mock.patch.object(StaffGradedAssignmentXBlock.get_student_item_dict,"__defaults__",(student_id,)),\ + self.dummy_upload("файл.txt") as (upload, expected): block.upload_assignment(mock.Mock(params={"assignment": upload})) response = block.staff_download( mock.Mock(params={"student_id": student["item"].student_id}) @@ -656,8 +652,9 @@ def test_staff_download_filename_with_spaces(self): self.personalize(block, **student) user = student["module"].student student_id = anonymous_id_for_user(user,self.course_id) - - with mock.patch.object(StaffGradedAssignmentXBlock.get_student_item_dict,"__defaults__",(student_id,)), self.dummy_upload(file_name) as (upload, expected): + + with mock.patch.object(StaffGradedAssignmentXBlock.get_student_item_dict,"__defaults__",(student_id,)),\ + self.dummy_upload(file_name) as (upload, expected): block.upload_assignment(mock.Mock(params={"assignment": upload})) response = block.staff_download( mock.Mock(params={"student_id": student["item"].student_id}) @@ -678,8 +675,9 @@ def test_file_download_comma_in_name(self, file_name): self.personalize(block, **student) user = student["module"].student student_id = anonymous_id_for_user(user,self.course_id) - - with mock.patch.object(StaffGradedAssignmentXBlock.get_student_item_dict,"__defaults__",(student_id,)), self.dummy_upload(file_name) as (upload, expected): + + with mock.patch.object(StaffGradedAssignmentXBlock.get_student_item_dict,"__defaults__",(student_id,)),\ + self.dummy_upload(file_name) as (upload, expected): block.upload_assignment(mock.Mock(params={"assignment": upload})) response = block.staff_download( mock.Mock(params={"student_id": student["item"].student_id}) @@ -695,7 +693,8 @@ def test_get_staff_grading_data_not_staff(self): test staff grading data for non staff members. """ block = self.make_one() - with mock.patch("edx_sga.sga.StaffGradedAssignmentXBlock.is_course_staff", return_value=False), self.assertRaises(PermissionDenied): + with mock.patch("edx_sga.sga.StaffGradedAssignmentXBlock.is_course_staff", return_value=False),\ + self.assertRaises(PermissionDenied): block.get_staff_grading_data(None) def test_get_staff_grading_data(self): @@ -934,7 +933,7 @@ def test_has_attempted(self): @data(True, False) def test_runtime_user_is_staff(self, is_staff): - + staff = StaffFactory.create(course_key=self.course.id) render.prepare_runtime_for_user( diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..54220678 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +DJANGO_SETTINGS_MODULE = edx_sga.test_settings +addopts = --cov . --ds=edx_sga.test_settings +norecursedirs = .git .tox edx_sga.static edx_sga.locale edx_sga.templates {arch} *.egg \ No newline at end of file diff --git a/run_devstack_integration_tests.sh b/run_devstack_integration_tests.sh index 9b85c10e..9589a9a5 100755 --- a/run_devstack_integration_tests.sh +++ b/run_devstack_integration_tests.sh @@ -8,6 +8,8 @@ mkdir -p reports pip install -r requirements/edx/testing.txt +pip install -e . + cd /edx-sga pip uninstall edx-sga -y pip install -e . diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index fe889355..00000000 --- a/setup.cfg +++ /dev/null @@ -1,177 +0,0 @@ -[tool:pytest] -# Note: The first file of settings found is used, there is no combining, so -# this file is only used for tests that don't find a pytest.ini file first. -# Details at https://docs.pytest.org/en/latest/reference/customize.html - -DJANGO_SETTINGS_MODULE = lms.envs.test -addopts = --nomigrations --reuse-db --durations=20 --json-report --json-report-omit keywords streams collectors log traceback tests --json-report-file=none -# Enable default handling for all warnings, including those that are ignored by default; -# but hide rate-limit warnings (because we deliberately don't throttle test user logins) -# and field_data deprecation warnings (because fixing them requires a major low-priority refactoring) -filterwarnings = - default - ignore:No request passed to the backend, unable to rate-limit:UserWarning - ignore::xblock.exceptions.FieldDataDeprecationWarning -junit_family = xunit2 -norecursedirs = .* *.egg build conf dist node_modules test_root cms/envs lms/envs -python_classes = -python_files = tests.py test_*.py tests_*.py *_tests.py __init__.py - -[pycodestyle] -# error codes: https://pycodestyle.readthedocs.io/en/latest/intro.html#error-codes -# E501: line too long -# E265: block comment should start with '# ' -# We ignore this because pep8 used to erroneously lump E266 into it also. -# We should probably fix these now. -# E266: too many leading '#' for block comment -# We have lots of comments that look like "##### HEADING #####" which violate -# this rule, because they don't have a space after the first #. However, -# they're still perfectly reasonable comments, so we disable this rule. -# W602: deprecated form of raising exception -# We do this in a few places to modify the exception message while preserving -# the traceback. See this blog post for more info: -# http://nedbatchelder.com/blog/200711/rethrowing_exceptions_in_python.html -# It's a little unusual, but we have good reasons for doing so, so we disable -# this rule. -# E305,E402,E722,E731,E741,E743,W503,W504: errors and warnings added since pep8/pycodestyle -# 1.5.7 that we haven't cleaned up yet -ignore=E265,E266,E305,E402,E501,E722,E731,E741,E743,W503,W504,W602 -exclude=migrations,.git,.pycharm_helpers,.tox,test_root/staticfiles,node_modules - -[isort] -indent=' ' -line_length=120 -multi_line_output=3 -skip= - envs - migrations - -[importlinter] -root_packages = - lms - cms - openedx -include_external_packages = True -contract_types = - # Our custom contract which checks that we're only importing from 'api.py' - # for participating packages. - isolated_apps: openedx.testing.importlinter.isolated_apps_contract.IsolatedAppsContract - -[importlinter:contract:1] -name = lms and cms are independent -type = independence -modules = - lms - cms -ignore_imports = - ############################################################################ - # lms side imports that we are ignoring for now - lms.djangoapps.course_home_api.outline.tests.test_view -> cms.djangoapps.contentstore.outlines - lms.djangoapps.courseware.plugins -> cms.djangoapps.contentstore.utils - lms.djangoapps.course_home_api.tests.utils -> cms.djangoapps.contentstore.outlines - # lms.djangoapps.instructor.tests.test_api & lms.djangoapps.instructor.tests.test_tools - # -> openedx.core.djangoapps.course_date_signals.handlers - # -> cms.djangoapps.contentstore.config.waffle - openedx.core.djangoapps.course_date_signals.handlers -> cms.djangoapps.contentstore.config.waffle - ############################################################################ - # cms side imports that we are ignoring for now - cms.djangoapps.contentstore.views.tests.test_block -> lms.djangoapps.lms_xblock.mixin - cms.envs.common -> lms.envs.common - cms.djangoapps.contentstore.signals.handlers -> lms.djangoapps.grades.api - cms.djangoapps.contentstore.course_group_config -> lms.lib.utils - cms.djangoapps.contentstore.views.preview -> lms.djangoapps.lms_xblock.field_data - cms.envs.common -> lms.djangoapps.lms_xblock.mixin - cms.envs.test -> lms.envs.test - # cms.djangoapps.contentstore.views.tests.test_group_configurations - # -> openedx.features.content_type_gating.helpers - # -> lms.djangoapps.courseware.masquerade - openedx.features.content_type_gating.helpers -> lms.djangoapps.courseware.masquerade - # cms.djangoapps.contentstore.utils - # -> openedx.core.djangoapps.django_comment_common.models - # -> openedx.core.djangoapps.course_groups.cohorts - # -> lms.djangoapps.courseware.courses - openedx.core.djangoapps.course_groups.cohorts -> lms.djangoapps.courseware.courses - # cms.djangoapps.models.settings.course_metadata - # -> openedx.features.course_experience - # -> openedx.features.course_experience.url_helpers - # -> lms.djangoapps.courseware.toggles - openedx.features.course_experience.url_helpers -> lms.djangoapps.courseware.toggles - # cms.djangoapps.contentstore.[various] - # -> openedx.features.content_type_gating.partitions - # -> lms.djangoapps.commerce.utils - openedx.features.content_type_gating.partitions -> lms.djangoapps.commerce.utils - # cms.djangoapps.contentstore.course_group_config - # -> openedx.core.djangoapps.course_groups.partition_scheme - # -> lms.djangoapps.courseware.masquerade - openedx.core.djangoapps.course_groups.partition_scheme -> lms.djangoapps.courseware.masquerade - # cms.djangoapps.contentstore.[various] & cms.djangoapps.coursegraph.[various] - # -> openedx.core.djangoapps.content.course_overviews.models - # -> lms.djangoapps.ccx.utils - # & lms.djangoapps.certificates.api - # & lms.djangoapps.discussion.django_comment_client - openedx.core.djangoapps.content.course_overviews.models -> lms.djangoapps.*.* - # cms.djangoapps.export_course_metadata.tasks - # -> openedx.core.djangoapps.schedules.content_highlights - # -> lms.djangoapps.courseware.block_render & lms.djangoapps.courseware.model_data - openedx.core.djangoapps.content_libraries.* -> lms.djangoapps.*.* - # cms.djangoapps.contentstore.tasks -> openedx.core.djangoapps.content_libraries.[various] - # -> lms.djangoapps.grades.api - openedx.core.djangoapps.xblock.*.* -> lms.djangoapps.*.* - # cms.djangoapps.contentstore.tasks -> openedx.core.djangoapps.content_libraries.[various] -> openedx.core.djangoapps.xblock.[various] - # -> lms.djangoapps.courseware & lms.djangoapps.courseware.grades - openedx.core.djangoapps.schedules.content_highlights -> lms.djangoapps.courseware.* - # cms.djangoapps.contentstore.[various] - # -> openedx.core.lib.gating.api - # -> lms.djangoapps.course_blocks.api & lms.djangoapps.courseware.access & lms.djangoapps.grades.api - openedx.core.lib.gating.api -> lms.djangoapps.*.* - # cms.djangoapps.contentstore.[various] - # -> openedx.features.content_type_gating.partitions - # -> openedx.features.discounts.utils - # -> lms.djangoapps.courseware.utils & lms.djangoapps.experiments.models - openedx.features.discounts.utils -> lms.djangoapps.courseware.utils - openedx.features.discounts.utils -> lms.djangoapps.experiments.models - # cms.djangoapps.contentstore.signals.handlers - # -> openedx.core.djangoapps.discussions.tasks - # -> openedx.core.djangoapps.discussions.utils - # -> lms.djangoapps.courseware.access - openedx.core.djangoapps.discussions.utils -> lms.djangoapps.courseware.access - # cms.djangoapps.contentstore.[various] - # -> openedx.features.content_type_gating.partitions - # -> openedx.features.discounts.utils - # -> openedx.features.discounts.applicability - # -> lms.djangoapps.courseware.toggles - # & lms.djangoapps.courseware.utils - # & lms.djangoapps.experiments.models - # & lms.djangoapps.experiments.stable_bucketing - openedx.features.discounts.applicability -> lms.djangoapps.courseware.* - openedx.features.discounts.applicability -> lms.djangoapps.experiments.* - # cms.djangoapps.contentstore.[various] - # -> openedx.core.djangoapps.content.learning_sequences.api - # -> openedx.core.djangoapps.content.learning_sequences.api.outlines - # -> openedx.core.djangoapps.content.learning_sequences.api.permissions - # -> lms.djangoapps.courseware.access - openedx.core.djangoapps.content.learning_sequences.api.permissions -> lms.djangoapps.courseware.access - # cms.djangoapps.contentstore.[various] - # -> openedx.features.content_type_gating.partitions - # -> openedx.features.discounts.utils - # -> openedx.features.discounts.applicability - # -> openedx.features.enterprise_support.utils - openedx.features.enterprise_support.utils -> lms.djangoapps.branding.api - cms.djangoapps.contentstore.rest_api.v1.views.settings -> lms.djangoapps.certificates.api - - -[importlinter:contract:2] -name = Do not depend on non-public API of isolated apps. -type = isolated_apps -isolated_apps = - openedx.core.djangoapps.agreements - openedx.core.djangoapps.bookmarks - openedx.core.djangoapps.content_libraries - openedx.core.djangoapps.content_staging - openedx.core.djangoapps.olx_rest_api - openedx.core.djangoapps.xblock - openedx.core.lib.xblock_serializer -allowed_modules = - # Only imports from api.py are allowed elsewhere in the code - # See https://open-edx-proposals.readthedocs.io/en/latest/best-practices/oep-0049-django-app-patterns.html#api-py - api