Skip to content

Commit 17bd3ff

Browse files
Update tutorial ci (#1344)
1 parent eca63be commit 17bd3ff

16 files changed

+412
-381
lines changed

Diff for: .github/workflows/run-tutorial.yml

+71-64
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ on:
99
pull_request:
1010
paths:
1111
- 'docs/tutorials/**/*.py'
12-
- '.github/workflows/run-tutorials.yml'
12+
- '.github/workflows/test-tutorials.yml'
1313

1414
# Allow manual trigger
1515
workflow_dispatch:
@@ -18,7 +18,7 @@ jobs:
1818
test-tutorials:
1919
runs-on: ubuntu-latest
2020
strategy:
21-
fail-fast: false
21+
fail-fast: false # This ensures all matrix combinations run even if one fails
2222
matrix:
2323
python-version: ["3.9"]
2424
tutorial-group:
@@ -34,23 +34,21 @@ jobs:
3434
uses: actions/setup-python@v5
3535
with:
3636
python-version: ${{ matrix.python-version }}
37-
cache: 'pip'
3837

3938
- name: Install dependencies
4039
run: |
41-
python -m pip install --upgrade pip
42-
# Install Gymnasium and its dependencies
43-
pip install -e .
44-
# Install additional dependencies for tutorials
45-
pip install torch torchvision tqdm matplotlib seaborn pandas pygame
46-
# Install MuJoCo dependencies if needed
4740
sudo apt-get update
48-
sudo apt-get install -y patchelf libosmesa6-dev libgl1-mesa-glx libglfw3 libglew-dev
41+
sudo apt-get install -y libglu1-mesa-dev libgl1-mesa-dev libosmesa6-dev xvfb unzip patchelf ffmpeg cmake swig
4942
50-
- name: Install MuJoCo (for MuJoCo tutorials)
43+
- name: Install Gymnasium basics tutorial requirements
44+
if: matrix.tutorial-group == 'gymnasium_basics'
45+
run: |
46+
pip install ".[mujoco, box2d]"
47+
48+
- name: Install Training Agents tutorial requirements
5149
if: matrix.tutorial-group == 'training_agents'
5250
run: |
53-
pip install mujoco gymnasium[mujoco]
51+
pip install ".[mujoco, toy_text, box2d]" torch seaborn matplotlib pandas tqdm
5452
5553
- name: Get changed files
5654
id: changed-files
@@ -59,6 +57,34 @@ jobs:
5957
files: docs/tutorials/**/*.py
6058
if: github.event_name == 'pull_request'
6159

60+
- name: Patch tutorials
61+
run: |
62+
# Patch load_quadruped_model.py to use the built-in ant.xml instead of the missing mujoco_menagerie file
63+
if [ -f "docs/tutorials/gymnasium_basics/load_quadruped_model.py" ]; then
64+
sed -i 's|"./mujoco_menagerie/unitree_go1/scene.xml"|"ant.xml"|g' docs/tutorials/gymnasium_basics/load_quadruped_model.py
65+
fi
66+
67+
# Patch mujoco_reinforce.py to reduce the number of episodes for CI
68+
if [ -f "docs/tutorials/training_agents/mujoco_reinforce.py" ]; then
69+
sed -i 's/total_num_episodes = int(5e3)/total_num_episodes = int(1e2)/g' docs/tutorials/training_agents/mujoco_reinforce.py
70+
fi
71+
72+
# Patch frozenlake_q_learning.py to reduce the number of episodes and runs
73+
if [ -f "docs/tutorials/training_agents/frozenlake_q_learning.py" ]; then
74+
sed -i 's/total_episodes=2000/total_episodes=200/g' docs/tutorials/training_agents/frozenlake_q_learning.py
75+
sed -i 's/n_runs=20/n_runs=3/g' docs/tutorials/training_agents/frozenlake_q_learning.py
76+
sed -i 's/map_sizes = \[4, 7, 9, 11\]/map_sizes = \[4, 7\]/g' docs/tutorials/training_agents/frozenlake_q_learning.py
77+
fi
78+
79+
# Patch vector_a2c.py to reduce the number of updates and environments
80+
if [ -f "docs/tutorials/training_agents/vector_a2c.py" ]; then
81+
sed -i 's/n_envs = 10/n_envs = 4/g' docs/tutorials/training_agents/vector_a2c.py
82+
sed -i 's/n_updates = 1000/n_updates = 100/g' docs/tutorials/training_agents/vector_a2c.py
83+
fi
84+
85+
# Make sure we use 'rgb_array' render mode instead of 'human' everywhere to avoid display issues
86+
find docs/tutorials -name "*.py" -type f -exec sed -i 's/render_mode="human"/render_mode="rgb_array"/g' {} \;
87+
6288
- name: Test tutorials (${{ matrix.tutorial-group }})
6389
id: run-tutorials
6490
run: |
@@ -67,7 +93,6 @@ jobs:
6793
6894
# Determine which tutorials to test
6995
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
70-
echo "PR detected - testing only modified tutorials"
7196
# Get the list of modified tutorial files in this group
7297
for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
7398
if [[ $file == docs/tutorials/${{ matrix.tutorial-group }}/* && $file == *.py ]]; then
@@ -77,14 +102,10 @@ jobs:
77102
78103
# If no tutorials in this group were modified, skip this job
79104
if [ ! -f tutorial_files.txt ] || [ ! -s tutorial_files.txt ]; then
80-
echo "No tutorials modified in ${{ matrix.tutorial-group }} - skipping tests"
81-
echo "total=0" >> $GITHUB_OUTPUT
82-
echo "passed=0" >> $GITHUB_OUTPUT
83-
echo "failed=0" >> $GITHUB_OUTPUT
105+
echo "No tutorials modified in ${{ matrix.tutorial-group }} - skipping"
84106
exit 0
85107
fi
86108
else
87-
echo "Main branch or manual run - testing all tutorials"
88109
# Find all Python files in the tutorial group
89110
find docs/tutorials/${{ matrix.tutorial-group }} -name "*.py" -type f | sort > tutorial_files.txt
90111
fi
@@ -94,81 +115,64 @@ jobs:
94115
passed=0
95116
failed=0
96117
97-
# Run each tutorial with timeout
118+
# Run each tutorial with timeout - continue even if one fails
98119
while IFS= read -r tutorial; do
120+
# Clear separator for better readability
121+
echo ""
122+
echo "========================================================"
99123
echo "Running tutorial: $tutorial"
124+
echo "========================================================"
100125
total=$((total+1))
101126
102127
# Set max time based on complexity (can be adjusted)
103128
max_time=300 # 5 minutes default
104129
105-
# Create a marker to skip rendering for headless environment
106-
sed -i 's/render_mode="human"/render_mode="rgb_array"/g' "$tutorial" || true
130+
# Create log file path
131+
log_file="test-results/$(basename "$tutorial").log"
107132
108133
# Run the tutorial with timeout and record results
109134
start_time=$(date +%s)
110-
timeout $max_time python "$tutorial" > "test-results/$(basename "$tutorial").log" 2>&1
135+
# Use set +e so the script continues even if the command fails
136+
set +e
137+
timeout $max_time python "$tutorial" > "$log_file" 2>&1
111138
exit_code=$?
139+
set -e
112140
end_time=$(date +%s)
113141
execution_time=$((end_time-start_time))
114142
143+
# Output results to console immediately
115144
if [ $exit_code -eq 0 ]; then
116-
echo "✅ Passed: $tutorial (${execution_time}s)"
145+
echo "✅ PASSED: $tutorial (${execution_time}s)"
117146
passed=$((passed+1))
118147
echo "$tutorial,pass,$execution_time" >> test-results/summary.csv
119148
elif [ $exit_code -eq 124 ]; then
120-
echo "⚠️ Timeout: $tutorial (exceeded ${max_time}s)"
149+
echo "⚠️ TIMEOUT: $tutorial (exceeded ${max_time}s)"
121150
failed=$((failed+1))
122151
echo "$tutorial,timeout,$max_time" >> test-results/summary.csv
152+
# Show the last output before timeout
153+
echo "Last output before timeout:"
154+
echo "----------------------------------------"
155+
tail -n 20 "$log_file"
156+
echo "----------------------------------------"
123157
else
124-
echo "❌ Failed: $tutorial (${execution_time}s)"
158+
echo "❌ FAILED: $tutorial (${execution_time}s)"
125159
failed=$((failed+1))
126160
echo "$tutorial,fail,$execution_time" >> test-results/summary.csv
161+
# Show the error details
162+
echo "Error details:"
163+
echo "----------------------------------------"
164+
cat "$log_file"
165+
echo "----------------------------------------"
127166
fi
128167
129-
echo "----------------------------------------"
130168
done < tutorial_files.txt
131169
132-
echo "::endgroup::"
133-
134-
# Set output variables
170+
# Export the counters as outputs for later steps
135171
echo "total=$total" >> $GITHUB_OUTPUT
136172
echo "passed=$passed" >> $GITHUB_OUTPUT
137173
echo "failed=$failed" >> $GITHUB_OUTPUT
138174
139-
# Generate summary
140-
echo "### Tutorial Test Results for ${{ matrix.tutorial-group }} 📊" >> $GITHUB_STEP_SUMMARY
141-
echo "" >> $GITHUB_STEP_SUMMARY
142-
143-
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
144-
echo "**Mode:** Testing only modified tutorials in PR #${{ github.event.pull_request.number }}" >> $GITHUB_STEP_SUMMARY
145-
else
146-
echo "**Mode:** Testing all tutorials (main branch or manual run)" >> $GITHUB_STEP_SUMMARY
147-
fi
148-
echo "" >> $GITHUB_STEP_SUMMARY
149-
150-
echo "| Metric | Count |" >> $GITHUB_STEP_SUMMARY
151-
echo "| ------ | ----- |" >> $GITHUB_STEP_SUMMARY
152-
echo "| ✅ Passed | $passed |" >> $GITHUB_STEP_SUMMARY
153-
echo "| ❌ Failed | $failed |" >> $GITHUB_STEP_SUMMARY
154-
echo "| 📚 Total | $total |" >> $GITHUB_STEP_SUMMARY
155-
156-
# List all tested tutorials
157-
if [ $total -gt 0 ]; then
158-
echo "" >> $GITHUB_STEP_SUMMARY
159-
echo "### Tested Tutorials 📝" >> $GITHUB_STEP_SUMMARY
160-
echo "" >> $GITHUB_STEP_SUMMARY
161-
162-
while IFS=, read -r file status time; do
163-
if [ "$status" == "pass" ]; then
164-
echo "- ✅ $file (${time}s)" >> $GITHUB_STEP_SUMMARY
165-
elif [ "$status" == "timeout" ]; then
166-
echo "- ⚠️ $file (timeout after ${time}s)" >> $GITHUB_STEP_SUMMARY
167-
else
168-
echo "- ❌ $file (failed after ${time}s)" >> $GITHUB_STEP_SUMMARY
169-
fi
170-
done < test-results/summary.csv
171-
fi
175+
echo "::endgroup::"
172176
173177
- name: Upload test results
174178
if: always()
@@ -184,8 +188,11 @@ jobs:
184188
if [ "${{ steps.run-tutorials.outputs.total }}" -eq 0 ]; then
185189
echo "::notice::No tutorials were tested in this group."
186190
elif [ "${{ steps.run-tutorials.outputs.failed }}" -gt 0 ]; then
187-
echo "::error::${{ steps.run-tutorials.outputs.failed }} out of ${{ steps.run-tutorials.outputs.total }} tutorials failed."
188-
exit 1
191+
echo "::warning::${{ steps.run-tutorials.outputs.failed }} out of ${{ steps.run-tutorials.outputs.total }} tutorials failed. Read in Test tutorials and click `Running tutorials...` to see the error messages."
189192
else
190193
echo "::notice::All ${{ steps.run-tutorials.outputs.total }} tutorials passed."
191194
fi
195+
196+
# This ensures the job reports failure if any tutorials failed,
197+
# but without stopping other jobs in the matrix
198+
[ "${{ steps.run-tutorials.outputs.failed }}" -eq 0 ]

Diff for: docs/tutorials/README.rst

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
Tutorials
22
=========
3-
In this section, we cover some of the most well-known benchmarks of RL including the Frozen Lake, Black Jack, and Training using REINFORCE for Mujoco.
43

5-
Additionally, we provide a guide on how to load custom quadruped robot environments, implementing custom wrappers, creating custom environments, handling time limits, and training A2C with Vector Envs and Domain Randomization.
4+
We provide two sets of tutorials: basics and training.
65

7-
Lastly, there is a guide on third-party integrations with Gymnasium.
6+
* The aim of the basics tutorials is to showcase the fundamental API of Gymnasium to help users implement it
7+
* The most common application of Gymnasium is for training RL agents, the training tutorials aim to show a range of example implementations for different environments
8+
9+
Additionally, we provide the third party tutorials as a link for external projects that utilise Gymnasium that could help users.

Diff for: docs/tutorials/gymnasium_basics/README.rst

+6-12
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,10 @@
1-
Gymnasium Basics Documentation Links
1+
Gymnasium Basics
22
----------------
3-
Load custom quadruped robot environments link: https://gymnasium.farama.org/tutorials/gymnasium_basics/load_quadruped_model/
4-
5-
Implementing Custom Wrappers link: https://gymnasium.farama.org/tutorials/gymnasium_basics/implementing_custom_wrappers/
6-
7-
Make your own custom environment(environment_creation.py): https://gymnasium.farama.org/tutorials/gymnasium_basics/environment_creation/
8-
9-
Handling Time Limits: https://gymnasium.farama.org/tutorials/gymnasium_basics/handling_time_limits/
10-
11-
Training A2C with Vector Envs and Domain Randomization: https://gymnasium.farama.org/tutorials/gymnasium_basics/vector_envs_tutorial/
123

134
.. toctree::
14-
:hidden:
5+
:hidden:
156

16-
/tutorials/gymnasium_basics/load_quadruped_model.md
7+
environment_creation
8+
implementing_custom_wrappers
9+
handling_time_limits
10+
load_quadruped_model

Diff for: docs/tutorials/gymnasium_basics/environment_creation.py

+10-10
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,16 @@
5858
5959
.
6060
├── gymnasium_env
61-
   ├── envs
62-
   │   ├── grid_world.py
63-
   │   └── __init__.py
64-
   ├── __init__.py
65-
   └── wrappers
66-
   ├── clip_reward.py
67-
   ├── discrete_actions.py
68-
   ├── __init__.py
69-
   ├── reacher_weighted_reward.py
70-
   └── relative_position.py
61+
├── envs
62+
├── grid_world.py
63+
└── __init__.py
64+
├── __init__.py
65+
└── wrappers
66+
├── clip_reward.py
67+
├── discrete_actions.py
68+
├── __init__.py
69+
├── reacher_weighted_reward.py
70+
└── relative_position.py
7171
├── LICENSE
7272
├── pyproject.toml
7373
└── README.md

Diff for: docs/tutorials/gymnasium_basics/implementing_custom_wrappers.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@
3232
# observation wrapper like this:
3333

3434
import numpy as np
35-
from gym import ActionWrapper, ObservationWrapper, RewardWrapper, Wrapper
3635

3736
import gymnasium as gym
37+
from gymnasium import ActionWrapper, ObservationWrapper, RewardWrapper, Wrapper
3838
from gymnasium.spaces import Box, Discrete
3939

4040

@@ -69,12 +69,12 @@ def action(self, act):
6969
return self.disc_to_cont[act]
7070

7171

72-
if __name__ == "__main__":
73-
env = gym.make("LunarLanderContinuous-v2")
74-
wrapped_env = DiscreteActions(
75-
env, [np.array([1, 0]), np.array([-1, 0]), np.array([0, 1]), np.array([0, -1])]
76-
)
77-
print(wrapped_env.action_space) # Discrete(4)
72+
env = gym.make("LunarLanderContinuous-v3")
73+
# print(env.action_space) # Box(-1.0, 1.0, (2,), float32)
74+
wrapped_env = DiscreteActions(
75+
env, [np.array([1, 0]), np.array([-1, 0]), np.array([0, 1]), np.array([0, -1])]
76+
)
77+
# print(wrapped_env.action_space) # Discrete(4)
7878

7979

8080
# %%

0 commit comments

Comments
 (0)