From 39fe1a20e45896bfbeb6234c2d2e1508e23a0eea Mon Sep 17 00:00:00 2001 From: Aarif Date: Thu, 18 Nov 2021 19:09:31 +0500 Subject: [PATCH] feat: move unit tests to github actions on self-hosted runners (#28873) --- .github/workflows/unit-tests.yml | 59 +++++++++++++++++++ .../workflows/verify-gha-unit-tests-count.yml | 54 +++++++++++++++++ .../course_modes/tests/test_admin.py | 6 +- lms/djangoapps/commerce/tests/test_signals.py | 5 +- scripts/gha_unit_tests_collector.py | 40 +++++++++++++ 5 files changed, 161 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/unit-tests.yml create mode 100644 .github/workflows/verify-gha-unit-tests-count.yml create mode 100644 scripts/gha_unit_tests_collector.py diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 000000000000..f177238b192f --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,59 @@ +name: unit-tests + +on: + pull_request: + push: + branches: + - master + +jobs: + run-tests: + runs-on: [ self-hosted ] + strategy: + matrix: + python-version: ['3.8'] + django-version: ["3.2"] + test_module: [ + "lms/djangoapps/badges/ lms/djangoapps/branding/ lms/djangoapps/bulk_email/ lms/djangoapps/bulk_enroll/ lms/djangoapps/bulk_user_retirement/ lms/djangoapps/ccx/ lms/djangoapps/certificates/ lms/djangoapps/commerce/", + "lms/djangoapps/course_api/ lms/djangoapps/course_blocks/ lms/djangoapps/course_goals/ lms/djangoapps/course_home_api/ lms/djangoapps/course_wiki/ lms/djangoapps/coursewarehistoryextended/ lms/djangoapps/dashboard/ lms/djangoapps/debug/", + "lms/djangoapps/courseware/", + "lms/djangoapps/discussion/ lms/djangoapps/edxnotes/ lms/djangoapps/email_marketing/ lms/djangoapps/experiments/", + "lms/djangoapps/gating/ lms/djangoapps/grades/ lms/djangoapps/instructor/ lms/djangoapps/instructor_analytics/", + "lms/djangoapps/instructor_task/ lms/djangoapps/learner_dashboard/ lms/djangoapps/lms_initialization/ lms/djangoapps/lms_xblock/ lms/djangoapps/lti_provider/ lms/djangoapps/mailing/ lms/djangoapps/mobile_api/ lms/djangoapps/monitoring/ lms/djangoapps/program_enrollments/ lms/djangoapps/rss_proxy/ lms/djangoapps/static_template_view/ lms/djangoapps/staticbook/ lms/djangoapps/support/ lms/djangoapps/survey/ lms/djangoapps/teams/ lms/djangoapps/tests/ lms/djangoapps/verify_student/ lms/envs/ lms/lib/ lms/tests.py", + "openedx/core/djangoapps/ace_common/ openedx/core/djangoapps/cors_csrf/ openedx/core/djangoapps/agreements/ openedx/core/djangoapps/api_admin/ openedx/core/djangoapps/auth_exchange/ openedx/core/djangoapps/bookmarks/ openedx/core/djangoapps/cache_toolbox/ openedx/core/djangoapps/catalog/ openedx/core/djangoapps/ccxcon/ openedx/core/djangoapps/commerce/ openedx/core/djangoapps/common_initialization/ openedx/core/djangoapps/common_views/ openedx/core/djangoapps/config_model_utils/ openedx/core/djangoapps/content/ openedx/core/djangoapps/content_libraries/ openedx/core/djangoapps/contentserver/ openedx/core/djangoapps/cookie_metadata/ openedx/core/djangoapps/course_apps/ openedx/core/djangoapps/course_date_signals/ openedx/core/djangoapps/course_groups/ openedx/core/djangoapps/coursegraph/ openedx/core/djangoapps/courseware_api/ openedx/core/djangoapps/crawlers/ openedx/core/djangoapps/credentials/ openedx/core/djangoapps/credit/ openedx/core/djangoapps/dark_lang/ openedx/core/djangoapps/debug/ openedx/core/djangoapps/demographics/ openedx/core/djangoapps/discussions/ openedx/core/djangoapps/django_comment_common/ openedx/core/djangoapps/embargo/ openedx/core/djangoapps/enrollments/ openedx/core/djangoapps/external_user_ids/", + "openedx/core/djangoapps/geoinfo/ openedx/core/djangoapps/header_control/ openedx/core/djangoapps/heartbeat/ openedx/core/djangoapps/lang_pref/ openedx/core/djangoapps/models/ openedx/core/djangoapps/monkey_patch/ openedx/core/djangoapps/oauth_dispatch/ openedx/core/djangoapps/olx_rest_api/ openedx/core/djangoapps/password_policy/ openedx/core/djangoapps/plugin_api/ openedx/core/djangoapps/plugins/ openedx/core/djangoapps/profile_images/ openedx/core/djangoapps/programs/ openedx/core/djangoapps/safe_sessions/ openedx/core/djangoapps/schedules/ openedx/core/djangoapps/self_paced/ openedx/core/djangoapps/service_status/ openedx/core/djangoapps/session_inactivity_timeout/ openedx/core/djangoapps/signals/ openedx/core/djangoapps/site_configuration/ openedx/core/djangoapps/system_wide_roles/ openedx/core/djangoapps/theming/ openedx/core/djangoapps/user_api/ openedx/core/djangoapps/user_authn/ openedx/core/djangoapps/util/ openedx/core/djangoapps/verified_track_content/ openedx/core/djangoapps/video_config/ openedx/core/djangoapps/video_pipeline/ openedx/core/djangoapps/waffle_utils/ openedx/core/djangoapps/xblock/ openedx/core/djangoapps/xmodule_django/ openedx/core/djangoapps/zendesk_proxy/ openedx/core/djangolib/ openedx/core/lib/ openedx/core/tests/ openedx/core/tests/ openedx/features/ openedx/testing/ openedx/tests/", + "cms/djangoapps/api/ cms/djangoapps/cms_user_tasks/ cms/djangoapps/course_creators/ cms/djangoapps/export_course_metadata/ cms/djangoapps/maintenance/ cms/djangoapps/models/ cms/djangoapps/pipeline_js/ cms/djangoapps/xblock_config/ cms/envs/ cms/lib/", + "cms/djangoapps/contentstore/", + "common/djangoapps/", + "common/lib/", + ] + + + name: python ${{ matrix.python-version }},django ${{ matrix.django-version }} ${{ matrix.test_module }} + steps: + - uses: actions/checkout@v2 + - name: start mongodb service + run: | + sudo /etc/init.d/mongodb start + + - name: set top-level module name + run: | + echo "module_name=$(echo '${{ matrix.test_module }}' | awk -F '/' '{print $1}')" >> $GITHUB_ENV + + - name: set settings path + run: | + echo "settings_path=$(if [ '${{ env.module_name }}' = 'cms' ]; then echo 'cms.envs.test'; else echo 'lms.envs.test' ; fi)" >> $GITHUB_ENV + +# - name: set pytest randomly option +# run: | +# echo "pytest_randomly_option=$(if [ '${{ env.module_name }}' = 'cms' ] || [ '${{ env.module_name }}' = 'common' ]; then echo '-p no:randomly'; else echo '' ; fi)" >> $GITHUB_ENV + + - name: install requirements + run: | + sudo pip install -r requirements/pip.txt + sudo pip install -r requirements/edx/testing.txt + sudo pip install "django~=${{ matrix.django-version }}.0" + + - name: run tests + run: | + python -Wd -m pytest -p no:randomly --ds=${{ env.settings_path }} ${{ matrix.test_module }} diff --git a/.github/workflows/verify-gha-unit-tests-count.yml b/.github/workflows/verify-gha-unit-tests-count.yml new file mode 100644 index 000000000000..f44337559ec9 --- /dev/null +++ b/.github/workflows/verify-gha-unit-tests-count.yml @@ -0,0 +1,54 @@ +name: verify unit tests count + +on: + pull_request: + push: + branches: + - master + +jobs: + collect-and-verify: + runs-on: [ self-hosted ] + steps: + - uses: actions/checkout@v2 + - name: install requirements + run: | + sudo pip install -r requirements/pip.txt + sudo pip install -r requirements/edx/testing.txt + + - name: collect tests from all modules + run: | + echo "root_cms_unit_tests_count=$(pytest --collect-only --ds=cms.envs.test cms/ -q | head -n -2 | wc -l)" >> $GITHUB_ENV + echo "root_lms_unit_tests_count=$(pytest --collect-only --ds=lms.envs.test lms/ openedx/ common/djangoapps/ common/lib/ -q | head -n -2 | wc -l)" >> $GITHUB_ENV + + - name: get GHA unit test paths + run: | + echo "cms_unit_test_paths=$(python scripts/gha_unit_tests_collector.py --cms-only)" >> $GITHUB_ENV + echo "lms_unit_test_paths=$(python scripts/gha_unit_tests_collector.py --lms-only)" >> $GITHUB_ENV + + + - name: collect tests from GHA unit test shards + run: | + echo "cms_unit_tests_count=$(pytest --collect-only --ds=cms.envs.test ${{ env.cms_unit_test_paths }} -q | head -n -2 | wc -l)" >> $GITHUB_ENV + echo "lms_unit_tests_count=$(pytest --collect-only --ds=lms.envs.test ${{ env.lms_unit_test_paths }} -q | head -n -2 | wc -l)" >> $GITHUB_ENV + + + - name: add unit tests count + run: | + echo "root_all_unit_tests_count=$((${{ env.root_cms_unit_tests_count }}+${{ env.root_lms_unit_tests_count }}))" >> $GITHUB_ENV + echo "shards_all_unit_tests_count=$((${{ env.cms_unit_tests_count }}+${{ env.lms_unit_tests_count }}))" >> $GITHUB_ENV + + - name: print unit tests count + run: | + echo CMS unit tests from root: ${{ env.root_cms_unit_tests_count }} + echo LMS unit tests from root: ${{ env.root_lms_unit_tests_count }} + echo CMS unit tests from shards: ${{ env.cms_unit_tests_count }} + echo LMS unit tests from shards: ${{ env.lms_unit_tests_count }} + echo All root unit tests count: ${{ env.root_all_unit_tests_count }} + echo All shards unit tests count: ${{ env.shards_all_unit_tests_count }} + + - name: verify unit tests count + if: ${{ env.root_all_unit_tests_count != env.shards_all_unit_tests_count }} + run: | + echo "::error title='Unit test modules in unit-tests.yml workflow are outdated'::unit tests running in unit-tests workflow don't match the count for unit tests for entire edx-platform suite, please update the test_module under matrix to add any missing apps and match the count" + exit 1 diff --git a/common/djangoapps/course_modes/tests/test_admin.py b/common/djangoapps/course_modes/tests/test_admin.py index c71d011f65c7..7a76646b2250 100644 --- a/common/djangoapps/course_modes/tests/test_admin.py +++ b/common/djangoapps/course_modes/tests/test_admin.py @@ -61,18 +61,20 @@ def test_expiration_timezone(self): response = self.client.post(reverse('admin:course_modes_coursemode_add'), data=data) self.assertRedirects(response, reverse('admin:course_modes_coursemode_changelist')) + course_mode = CourseMode.objects.get(course_id=str(course.id), mode_slug='verified') + # Verify that datetime is appears on list page response = self.client.get(reverse('admin:course_modes_coursemode_changelist')) self.assertContains(response, get_time_display(expiration, '%B %d, %Y, %H:%M %p')) # Verify that on the edit page the datetime value appears as UTC. - resp = self.client.get(reverse('admin:course_modes_coursemode_change', args=(1,))) + resp = self.client.get(reverse('admin:course_modes_coursemode_change', args=(course_mode.id,))) self.assertContains(resp, expiration.date()) self.assertContains(resp, expiration.time()) # Verify that the expiration datetime is the same as what we set # (hasn't changed because of a timezone translation). - course_mode = CourseMode.objects.get(pk=1) + course_mode.refresh_from_db() assert course_mode.expiration_datetime.replace(tzinfo=None) == expiration.replace(tzinfo=None) diff --git a/lms/djangoapps/commerce/tests/test_signals.py b/lms/djangoapps/commerce/tests/test_signals.py index 6f746649fef3..e93917114cee 100644 --- a/lms/djangoapps/commerce/tests/test_signals.py +++ b/lms/djangoapps/commerce/tests/test_signals.py @@ -323,4 +323,7 @@ def test_create_zendesk_ticket(self): 'tags': ['LMS'] + tags } } - self.assertDictEqual(json.loads(last_request.body.decode('utf8')), expected) + response_dict = json.loads(last_request.body.decode('utf8')) + response_dict['ticket']['tags'].sort() + expected['ticket']['tags'].sort() + self.assertDictEqual(response_dict, expected) diff --git a/scripts/gha_unit_tests_collector.py b/scripts/gha_unit_tests_collector.py new file mode 100644 index 000000000000..710e89d914aa --- /dev/null +++ b/scripts/gha_unit_tests_collector.py @@ -0,0 +1,40 @@ +import sys +import os +import yaml +import argparse + + +def get_all_unit_test_modules(): + unit_tests_yml = f'{os.getcwd()}/.github/workflows/unit-tests.yml' + with open(unit_tests_yml) as file: + unit_test_workflow_yaml = yaml.safe_load(file) + + return unit_test_workflow_yaml['jobs']['run-tests']['strategy']['matrix']['test_module'] + + +def get_modules_except_cms(): + all_unit_test_modules = get_all_unit_test_modules() + return [module for module in all_unit_test_modules if not module.startswith('cms')] + + +def get_cms_modules(): + all_unit_test_modules = get_all_unit_test_modules() + return [module for module in all_unit_test_modules if module.startswith('cms')] + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--cms-only", action="store_true", default="") + parser.add_argument("--lms-only", action="store_true", default="") + + argument = parser.parse_args() + + if argument.lms_only: + modules = get_modules_except_cms() + elif argument.cms_only: + modules = get_cms_modules() + else: + modules = get_all_unit_test_modules() + + unit_test_paths = ' '.join(modules) + sys.stdout.write(unit_test_paths)