diff --git a/README.md b/README.md index c23f015..457e186 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -# Integration Test in Tutor +# Open edX Plugin Integration Tests with Tutor -A Github action to test your Django Apps in Tutor (Open edX distribution). +A GitHub Action to test your Open edX plugin (Django app) within Tutor (Open edX distribution). This action automates the setup of a Tutor environment, installs your plugin, runs migrations, and executes your integration tests in an isolated environment. -## Example usage +## Example Usage -``` -name: Integration +```yaml +name: Integration Tests on: [pull_request] jobs: @@ -14,30 +14,174 @@ jobs: strategy: matrix: - tutor_version: ["<17.0.0", "==17.0.3", "<18.0.0"] + tutor_version: ["<18.0.0", "==18.0.3", "<19.0.0", "nightly"] steps: - - uses: actions/checkout@v4 - with: - path: eox-hooks - - - uses: eduNEXT/integration-test-in-tutor + - name: Run Integration Tests + uses: eduNEXT/integration-test-in-tutor@main with: + app_name: "eox-test" tutor_version: ${{ matrix.tutor_version }} - app_name: "eox-hooks" - shell_file_to_run: "eox_hooks/tests/tutor/integration.sh" + shell_file_to_run: "tests/integration.sh" + openedx_extra_pip_requirements: "package1==1.0 package2>=2.0" + fixtures_file: "fixtures/test_data.json" + openedx_imports_test_file_path: "eox_test/edxapp_wrapper/test_backends.py" ``` ## Inputs ### `app_name` -**Required** Application name to test. E.g., eox-tenant. +**Required** +The name of your plugin/application to test. This should match the directory name of your plugin. +* *Example*: `"eox-tenant"` ### `tutor_version` -**Required** The tutor version matrix to use. +**Required** +The version of Tutor where you want to run the integration tests. You can specify: + +* A specific version number (e.g., `"==18.0.3"`). +* A comparison operator with a version (e.g., `"<18.0.0"`). +* The string `"nightly"` to use the latest development version. + +> [!IMPORTANT] +> This action is officially supported and tested with Tutor versions corresponding to the current and immediate previous Open edX releases, as well as the nightly build. Using other Tutor versions is not guaranteed to be supported. ### `shell_file_to_run` -(Optional) The path of the shell file to run the integration tests. +**Optional** +The path to the shell script that runs your integration tests. This path is relative to your plugin directory. +* *Default*: `"scripts/execute_integration_tests.sh"` +* *Example*: `"tests/integration.sh"` + +### `openedx_extra_pip_requirements` + +**Optional** +Extra pip requirements to install in Open edX. These are additional Python packages your plugin depends on. Provide them as a space-separated string. +* *Default*: `""` (empty string) +* *Example*: `"package1==1.0 package2>=2.0"` + +### `fixtures_file` + +**Optional** +The path to the fixtures file of your plugin to load initial data for the tests. This path is relative to your plugin directory. +* *Example*: `"fixtures/test_data.json"` + +### `openedx_imports_test_file_path` + +**Optional** +The path to the Python file in your plugin that contains the test function for validating Open edX imports. This path is relative to your plugin directory. +* *Example*: `"eox_test/edxapp_wrapper/test_backends.py"` + +## Overview + +This GitHub Action automates the process of setting up a Tutor Open edX environment to test your plugin. It performs the following steps: + +1. **Checkout Plugin Code**: Checks out your plugin code into a directory specified by `app_name`. + +2. **Adjust Permissions**: Modifies file permissions to ensure that all files and directories are accessible, preventing permission-related errors during Tutor operations. + +3. **Set Tutor Environment Variables**: Sets necessary environment variables for Tutor. + +4. **Create Virtual Environments**: Creates isolated Python virtual environments for installing Tutor and for running the integration tests. + +5. **Install and Prepare Tutor**: Installs the specified version of Tutor and launches the Open edX platform. + + - If `tutor_version` is set to `"nightly"`, clones the Tutor repository from the `nightly` branch. + - Saves Tutor configuration. + - Launches Tutor in interactive mode. + +6. **Configure Caddyfile and Open edX Settings**: Configures the web server and Open edX settings using Tutor plugins, to enable running integration tests from the plugin with multiple sites. + +7. **Add Mount for Plugin**: Mounts your plugin into the LMS and CMS containers. + +8. **Restart Tutor Services**: Stops and starts Tutor services to apply the new mounts. + +9. **Install Open edX Plugin as an Editable Package**: Installs your plugin in editable mode inside both LMS and CMS containers. + +10. **Install Extra Requirements**: Installs any additional Python packages specified in `openedx_extra_pip_requirements`. + +11. **Run Migrations and Restart Services**: Applies database migrations and restarts Tutor services. + +12. **Import Demo Course**: Imports the Open edX demo course for testing purposes. + +13. **Test Open edX Imports in Plugin** *(Optional)*: Runs pytest to validate Open edX imports in your plugin if `openedx_imports_test_file_path` is provided. The only two dependencies installed to run these tests are `pytest` and `pytest-django`, so ensure that your import tests do not require any extra packages that are not in the plugin's base requirements. + +14. **Load Initial Data for the Tests** *(Optional)*: Loads initial data from a fixtures file into the LMS if `fixtures_file` is provided. + +15. **Check LMS Heartbeat**: Verifies that the LMS is running by hitting the heartbeat endpoint. + +16. **Set `DEMO_COURSE_ID` Environment Variable**: + Sets the `DEMO_COURSE_ID` environment variable based on the Tutor version. This variable allows you to refer to the demo course in your tests, which can be helpful when you need to interact with course content during testing. + + **Usage in Your Tests**: + In your test code, you can access the `DEMO_COURSE_ID` environment variable to get the identifier of the demo course. For example: + + ```python + import os + + DEMO_COURSE_ID = os.environ.get("DEMO_COURSE_ID") + # Use DEMO_COURSE_ID in your tests + ``` + +17. **Run Integration Tests**: Activates the test virtual environment and runs your integration tests using the specified shell script. + +## Notes + +- **Using the `"nightly"` Version of Tutor**: + The `"nightly"` option allows you to test your plugin against the latest development code of Tutor. This is useful for ensuring compatibility with upcoming features or changes. Be aware that the nightly version may be less stable and could introduce breaking changes. When using `"nightly"`, the action clones the Tutor repository from the `nightly` branch and uses pre-built Tutor images that are published daily to Docker Hub. These images include the latest changes from the master branch at the time of the build. This approach significantly reduces the execution time and conserves the runner's resources by eliminating the overhead of building images during the workflow. + +- **Paths**: Ensure that the paths provided in the inputs are relative to your plugin directory. + +- **Optional Steps**: Steps involving `openedx_imports_test_file_path` and `fixtures_file` are optional and will only run if the corresponding inputs are provided. + +- **Tutor Versions**: Use the matrix strategy to test your plugin against multiple Tutor versions, including the nightly build. + +- **Dependencies**: If your integration tests require additional dependencies, specify them in `openedx_extra_pip_requirements` or handle them within your `shell_file_to_run`. + +- **Maintaining Python Versions**: + + It's crucial to align the Python version in your virtual environments with the versions supported by the specified Tutor versions. Mismatched Python versions can lead to unexpected errors during Tutor operations and plugin integrations. + + If you need to specify a Python version different from the default provided by the runner, you can add a step before invoking the action to set the desired Python version. For example: + + ```yaml + - name: Set Up Python + uses: actions/setup-python@v4 + with: + python-version: '3.X' # Specify the required Python version here + + - name: Run Integration Tests + uses: eduNEXT/integration-test-in-tutor@mjh/run-integration-tests-outside-container + with: + ... + ``` + + If you're testing against multiple Tutor versions that require different Python versions, you can consider expanding your matrix to include Python versions. For example: + + ```yaml + strategy: + matrix: + include: + - tutor_version: '<18.0.0' + python_version: '3.8' + - tutor_version: '<19.0.0' + python_version: '3.9' + - tutor_version: 'nightly' + python_version: '3.10' + ``` + + Then, adjust the steps to use both `matrix.tutor_version` and `matrix.python_version`. + + ```yaml + steps: + - name: Set Up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python_version }} # Utilize matrix.python_version + ... + ``` + +## Contributing +Contributions are welcome! Please open an issue or submit a pull request for any enhancements or bug fixes. diff --git a/action.yml b/action.yml index 76097f1..17b0bdb 100644 --- a/action.yml +++ b/action.yml @@ -1,60 +1,179 @@ -name: Integration Test in Tutor -description: 'A Github action to test your plugin in Tutor (Open edX distribution)' +name: Open edX Plugin Integration Tests with Tutor +description: "A Github action to test your Open edX plugin in Tutor" inputs: app_name: - description: 'Application name to test. E.g., eox-tenant.' + description: "Open edX plugin or application name to test. E.g., eox-tenant." required: true tutor_version: - description: 'The tutor version matrix to use.' + description: "The tutor version matrix to use." required: true shell_file_to_run: - description: 'The path of the shell file to run the integration tests.' + description: "The path of the shell file to run the integration tests." + required: true + default: "scripts/execute_integration_tests.sh" + openedx_extra_pip_requirements: + description: "Optional extra pip requirements to install in Open edX. E.g: 'package1==1.0 package2>=2.0'" + required: false + default: "" + fixtures_file: + description: "Optional path to the plugin's fixtures file to load." + required: false + openedx_imports_test_file_path: + description: "Path to the file that contains the test function for validating Open edX imports. This should be a Python file within your project." required: false runs: - using: 'composite' + using: "composite" steps: - - name: Create a Virtual Environment - run: | - pip install virtualenv - virtualenv venv - shell: bash - - - name: Install Tutor - run: | - source venv/bin/activate - if [ "$INPUT_TUTOR_VERSION" = "nightly" ]; then - git clone --branch=nightly --depth=1 https://github.com/overhangio/tutor.git - chmod 777 tutor -R - pip install -e "./tutor[full]" - else - pip install "tutor$INPUT_TUTOR_VERSION" - fi - shell: bash - env: - INPUT_TUTOR_VERSION: ${{ inputs.tutor_version }} - - - name: Configure Tutor and Launch - run: | - source venv/bin/activate - TUTOR_ROOT="$(pwd)" tutor --version - TUTOR_ROOT="$(pwd)" tutor config save - TUTOR_ROOT="$(pwd)" tutor mounts add lms,cms,lms-worker,cms-worker:$(pwd)/$INPUT_APP:/openedx/$INPUT_APP - chmod 777 . -R - if [ "$INPUT_TUTOR_VERSION" = "nightly" ]; then - TUTOR_ROOT="$(pwd)" tutor images build openedx - fi - TUTOR_ROOT="$(pwd)" tutor local launch -I - shell: bash - env: - INPUT_APP: ${{ inputs.app_name }} - INPUT_TUTOR_VERSION: ${{ inputs.tutor_version }} - - - name: Run integration tests in lms - if: ${{ inputs.shell_file_to_run }} - run: | - source venv/bin/activate - TUTOR_ROOT="$(pwd)" tutor local run lms bash $INPUT_SHELL_FILE - shell: bash - env: - INPUT_SHELL_FILE: /openedx/${{ inputs.app_name }}/${{ inputs.shell_file_to_run }} + - name: Checkout code + uses: actions/checkout@v4 + with: + path: ${{ inputs.app_name }} + + - name: Adjust permissions to execute Tutor commands + run: | + chmod 777 . -R + shell: bash + + - name: Set Tutor environment variables + run: | + cat <> "$GITHUB_ENV" + LMS_HOST=local.edly.io + CMS_HOST=studio.local.edly.io + TUTOR_ROOT=$(pwd) + TUTOR_PLUGINS_ROOT=$(pwd)/plugins/ + EOF + shell: bash + + - name: Create virtualenvs + run: | + echo "Creating isolated venv for Tutor" + python -m venv .tutor_venv + echo "Creating isolated venv to run the integration tests" + python -m venv .tests_venv + shell: bash + + - name: Install and prepare Tutor + env: + INPUT_TUTOR_VERSION: ${{ inputs.tutor_version }} + run: | + source .tutor_venv/bin/activate + + if [ "$INPUT_TUTOR_VERSION" = "nightly" ]; then + git clone --branch=nightly --depth=1 https://github.com/overhangio/tutor.git + pip install -e ./tutor + else + pip install "tutor$INPUT_TUTOR_VERSION" + fi + + tutor config save --set LMS_HOST=$LMS_HOST --set CMS_HOST=$CMS_HOST + tutor local launch -I + shell: bash + + - name: Configure Caddyfile and Open edX settings + run: | + source .tutor_venv/bin/activate + + mkdir -p plugins + cp $GITHUB_ACTION_PATH/patches.yml plugins/ + tutor plugins enable patches + shell: bash + + - name: Add mount for Open edX plugin + run: | + source .tutor_venv/bin/activate + + tutor mounts add "lms:$GITHUB_WORKSPACE/${{ inputs.app_name }}:/openedx/${{ inputs.app_name }}" + tutor mounts add "cms:$GITHUB_WORKSPACE/${{ inputs.app_name }}:/openedx/${{ inputs.app_name }}" + shell: bash + + - name: Recreate containers to apply mounts + run: | + source .tutor_venv/bin/activate + + tutor local start -d lms cms + shell: bash + + - name: Install Open edX plugin as an editable package + run: | + source .tutor_venv/bin/activate + + tutor local exec lms pip install -e /openedx/${{ inputs.app_name }}/ + tutor local exec cms pip install -e /openedx/${{ inputs.app_name }}/ + shell: bash + + - name: Install extra requirements + run: | + source .tutor_venv/bin/activate + + tutor local exec lms pip install ${{ inputs.openedx_extra_pip_requirements }} + tutor local exec cms pip install ${{ inputs.openedx_extra_pip_requirements }} + shell: bash + + - name: Run migrations and restart services + run: | + source .tutor_venv/bin/activate + + tutor local exec lms python manage.py lms migrate + tutor local exec cms python manage.py cms migrate + tutor local restart + shell: bash + + - name: Import Demo course + run: | + source .tutor_venv/bin/activate + + tutor local do importdemocourse + shell: bash + + - name: Test Open edX imports in plugin + if: ${{ inputs.openedx_imports_test_file_path }} + run: | + source .tutor_venv/bin/activate + + tutor local exec lms bash -c "pip install pytest pytest-django" + tutor local exec lms bash -c "pytest -s --ds=lms.envs.tutor.test /openedx/${{ inputs.app_name }}/${{ inputs.openedx_imports_test_file_path }}" + shell: bash + + - name: Load initial data for the tests + if: ${{ inputs.fixtures_file }} + run: | + source .tutor_venv/bin/activate + + echo "Copying fixtures file to the LMS container" + tutor local exec lms python manage.py lms loaddata /openedx/${{ inputs.app_name }}/${{ inputs.fixtures_file }} + shell: bash + + - name: Curl Heartbeat + run: | + echo "Curling LMS heartbeat" + status_code=$(curl -s -o /dev/null -w "%{http_code}" http://$LMS_HOST/heartbeat) + if [ "$status_code" -ne 200 ]; then + echo "Error: LMS Heartbeat endpoint returned status code $status_code" + exit 1 + else + echo "Heartbeat endpoint returned status code 200" + fi + shell: bash + + - name: Set DEMO_COURSE_ID environment variable + run: | + #TODO: remove this once we stop supporting Tutor <18.0.0 + if [ "${{ inputs.tutor_version }}" = "<18.0.0" ]; then + echo "DEMO_COURSE_ID=course-v1:edX+DemoX+Demo_Course" >> $GITHUB_ENV + else + echo "DEMO_COURSE_ID=course-v1:OpenedX+DemoX+DemoCourse" >> $GITHUB_ENV + fi + shell: bash + + - name: Run integration tests + if: ${{ inputs.shell_file_to_run }} + env: + DEMO_COURSE_ID: ${{ env.DEMO_COURSE_ID }} + run: | + source .tests_venv/bin/activate + + cd ${{ inputs.app_name }} + chmod +x ./${{ inputs.shell_file_to_run }} + ./${{ inputs.shell_file_to_run }} + shell: bash diff --git a/patches.yml b/patches.yml new file mode 100644 index 0000000..f7f23f2 --- /dev/null +++ b/patches.yml @@ -0,0 +1,12 @@ +name: patches +patches: + caddyfile: | + {$default_site_port} { + import proxy "lms:8000" + } + openedx-cms-production-settings: | + ALLOWED_HOSTS = ["*"] + ALLOWED_AUTH_APPLICATIONS = ['cms-sso', 'cms-sso-dev'] + openedx-lms-production-settings: | + ALLOWED_HOSTS = ["*"] + ALLOWED_AUTH_APPLICATIONS = ['cms-sso', 'cms-sso-dev']