Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Tests

on:
push:
branches: [master]
pull_request:

jobs:
pytest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install dependencies
run: pip install -r requirements.txt

- name: Run tests
run: pytest
9 changes: 5 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,12 @@ When your contribution is ready, submit a pull request. Include a clear summary

## Testing and Continuous Integration (CI)

PyRPOD uses automated testing to ensure stability and prevent regressions. To facilitate this:
PyRPOD uses [pytest](https://docs.pytest.org/) to ensure stability and prevent regressions. To facilitate this:

1. Write unit tests for your contributions in the `tests` directory.
2. Ensure all tests pass before submitting a pull request.
3. If possible, run CI tests locally before pushing your changes. Follow the instructions in the project’s CI setup documentation.
1. Write tests for your contributions in the `tests` directory, following the existing `<group>_<category>_test_NN.py` naming convention (e.g. `rpod_unit_test_04.py`).
2. Run the full suite locally with `pytest` from the repository root before submitting a pull request.
3. Tests are automatically tagged with `unit`, `integration`, `verification`, and subsystem (`mdao`, `mission`, `plume`, `rpod`) markers based on their filename, so you can run a subset with e.g. `pytest -m unit` or `pytest -m rpod`.
4. CI runs the same `pytest` suite automatically on every push and pull request via [GitHub Actions](.github/workflows/tests.yml).

## Code Formatting

Expand Down
20 changes: 13 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,21 @@ PyRPOD utilizies scientific libraries such as NumPy, SciPy, Matplotlib, and SymP
pip install -r requirements.txt
```

3. Run all test cases.
3. Run all test cases.
```bash
cd tests/
python3 test_all.py
python -m pytest
```
It is recomended to use `python -m pytest` rather than the bare `pytest` command. This guarantees the
`python` you're currently using (venv, conda env, etc.) runs the tests, avoiding
`ModuleNotFoundError`s that show up if a system-wide `pytest` (e.g. installed via
`apt`) ends up on your `PATH` instead of the one in the environment where you ran
`pip install -r requirements.txt`.

3. Run individual test cases (Not ideal, I know).
4. Run a subset of test cases using markers (auto-tagged by category and subsystem).
```bash
cd tests/
mv <test_module>/test_case.py .
python3 test_case.py
python -m pytest -m unit # unit tests only
python -m pytest -m integration # integration tests only
python -m pytest -m verification # verification tests only
python -m pytest -m rpod # a specific subsystem (mdao, mission, plume, rpod)
python -m pytest tests/rpod # or just point pytest at a directory/file directly
```
15 changes: 15 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,18 @@ show_error_codes = true
"tests/**" = ["D", "S101"]
"**/__pycache__/**" = ["ALL"]
"venv-pyrpod/**" = ["ALL"]

[tool.pytest.ini_options]
testpaths = ["tests"]
# Legacy test files are named "<group>_<category>_test_NN.py" rather than pytest's default patterns.
python_files = ["test_*.py", "*_test_*.py"]
pythonpath = ["."]
markers = [
"unit: unit-level test cases",
"integration: integration test cases",
"verification: verification/validation test cases",
"mdao: mdao subsystem tests",
"mission: mission subsystem tests",
"plume: plume subsystem tests",
"rpod: rpod subsystem tests",
]
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ mypy
black
ruff
flake8
isort
isort
pytest
40 changes: 40 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import os
from pathlib import Path

import pytest

TESTS_DIR = Path(__file__).resolve().parent

CATEGORIES = ("unit", "integration", "verification")
GROUPS = ("mdao", "mission", "plume", "rpod")

# These files import a flat `pyrpod.<Module>` layout that no longer exists after
# the rpod -> plume/vehicle refactor. Excluded from collection until they are
# fixed or removed outright.
collect_ignore = ["old", "test_case_25.py", "rpod_verification_test_05.py"]


@pytest.fixture(autouse=True, scope="session")
def _run_from_tests_dir():
# Legacy test cases assume the process CWD is this `tests/` directory,
# e.g. `case_dir = '../case/...'` and `open('rpod/rpod_int_test_02...')`.
# Reproduce that historical invocation so paths resolve without editing
# every test file's path strings.
previous_cwd = os.getcwd()
os.chdir(TESTS_DIR)
yield
os.chdir(previous_cwd)


def pytest_collection_modifyitems(items):
for item in items:
stem = Path(item.fspath).stem
relative_parts = Path(item.fspath).resolve().relative_to(TESTS_DIR).parts

for category in CATEGORIES:
if f"_{category}_test" in stem:
item.add_marker(getattr(pytest.mark, category))

for group in GROUPS:
if group in relative_parts or stem.startswith(f"{group}_"):
item.add_marker(getattr(pytest.mark, group))
1 change: 0 additions & 1 deletion tests/mdao/mdao_integration_test_01.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
# ========================
# Write test case description.

import test_header
import unittest

class MDAOTest(unittest.TestCase):
Expand Down
1 change: 0 additions & 1 deletion tests/mdao/mdao_unit_test_01.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
# ========================
# Write test case description.

import test_header
import unittest

class MDAOTest(unittest.TestCase):
Expand Down
199 changes: 99 additions & 100 deletions tests/mdao/mdao_unit_test_02.py
Original file line number Diff line number Diff line change
@@ -1,101 +1,100 @@
# Juan P. Roldan
# University of Central Florida
# Department of Mechanical and Aerospace Engineering
# Last Changed: 03-28-24

# ========================
# PyRPOD: test/mdao/mdao_unit_test_03.py
# ========================
# A test case to create array of cant angle swept thruster configurations.
# The sweep assumes the given thrusters angle symmetrically.
# This means that opposite pitch thrusters angle opposite but equally to each other
# and yaw thrusters angle opposite but equally to each other
# !!currently, the pitch and yaw thrusters are canted in unison!!
# pitch thrusters are not angled such that they can produce a yaw
# yaw thrusters are not angled such that they can introduce a pitch

# TODO: Re-factor code to save data in a relevant object. Also add files to save to.

import test_header
import unittest, os, sys
import numpy as np

from pyrpod.rpod import JetFiringHistory, PlumeStrikeEstimationStudy
from pyrpod.vehicle import TargetVehicle, VisitingVehicle
from pyrpod.mdao import SweepConfig


class CoordinateSweepCheck(unittest.TestCase):
def test_cant_sweep(self):

# # number of deceleration thrusters to evenly distribute
# # !!!IMPORTANT!!! also ensure to update jfh, fire all thrusters one wants to visualize!!!
# nthrusters = 7

# # define the LM's radius
# r = 2 # m

# # creating an example configuration

# # identity matrix as the default for all thrusters
# # techically not needed, since all the thrusters are set to -x group
# # these thrusters DCMs are standardized within SweepConfig
# dcm = np.eye(3)

# # creating example thruster groups, to confirm thrusters that are for decel
# thruster_groups = {
# '+x': [],
# 'neg_x': [],
# '+y': [],
# '-y': [],
# '+z': [],
# '-z': [],
# '+pitch': [],
# '-pitch': [],
# '+yaw' : [],
# '-yaw' : []
# }

# config = {}

# # name each thruster, as corresponding to different packs
# # evenly distribute the exit coordinate of each thruster
# # append each thruster to deceleration group
# for i in range(1, nthrusters+1):
# name = 'P' + str(i) + 'T1'
# exit = [0, r*np.cos((i-1)*(2 * np.pi / nthrusters)), r*np.sin((i-1)*(2 * np.pi / nthrusters))]
# config[name] = {'name': [name], 'type': ['001'], 'exit': [exit], 'dcm': dcm}
# thruster_groups['neg_x'].append(name)

# # define step sizes for each angle
# dcant = 10 # deg

# # create SweepAngles object
# angle_sweep = SweepConfig.SweepDecelAngles(config, thruster_groups)

# # call sweep_long_thruster on the configuration and print the DCM's
# config_swept_array = angle_sweep.sweep_decel_thrusters_all(dcant)

# for i, config in enumerate(config_swept_array):
# # Path to directory holding data assets and results for a specific RPOD study.
# case_dir = '../case/mdao/cant_sweep/'

# # Load JFH data.
# jfh = JetFiringHistory.JetFiringHistory(case_dir)
# jfh.read_jfh()

# tv = TargetVehicle.TargetVehicle(case_dir)
# tv.set_stl()

# vv = VisitingVehicle.VisitingVehicle(case_dir)
# vv.set_stl()
# vv.set_thruster_config(config)

# rpod = RPOD.RPOD(case_dir)
# rpod.study_init(jfh, tv, vv)

# rpod.visualize_sweep(i)
return

if __name__ == '__main__':
# Juan P. Roldan
# University of Central Florida
# Department of Mechanical and Aerospace Engineering
# Last Changed: 03-28-24

# ========================
# PyRPOD: test/mdao/mdao_unit_test_03.py
# ========================
# A test case to create array of cant angle swept thruster configurations.
# The sweep assumes the given thrusters angle symmetrically.
# This means that opposite pitch thrusters angle opposite but equally to each other
# and yaw thrusters angle opposite but equally to each other
# !!currently, the pitch and yaw thrusters are canted in unison!!
# pitch thrusters are not angled such that they can produce a yaw
# yaw thrusters are not angled such that they can introduce a pitch

# TODO: Re-factor code to save data in a relevant object. Also add files to save to.

import unittest, os, sys
import numpy as np

from pyrpod.rpod import JetFiringHistory, PlumeStrikeEstimationStudy
from pyrpod.vehicle import TargetVehicle, VisitingVehicle
from pyrpod.mdao import SweepConfig


class CoordinateSweepCheck(unittest.TestCase):
def test_cant_sweep(self):

# # number of deceleration thrusters to evenly distribute
# # !!!IMPORTANT!!! also ensure to update jfh, fire all thrusters one wants to visualize!!!
# nthrusters = 7

# # define the LM's radius
# r = 2 # m

# # creating an example configuration

# # identity matrix as the default for all thrusters
# # techically not needed, since all the thrusters are set to -x group
# # these thrusters DCMs are standardized within SweepConfig
# dcm = np.eye(3)

# # creating example thruster groups, to confirm thrusters that are for decel
# thruster_groups = {
# '+x': [],
# 'neg_x': [],
# '+y': [],
# '-y': [],
# '+z': [],
# '-z': [],
# '+pitch': [],
# '-pitch': [],
# '+yaw' : [],
# '-yaw' : []
# }

# config = {}

# # name each thruster, as corresponding to different packs
# # evenly distribute the exit coordinate of each thruster
# # append each thruster to deceleration group
# for i in range(1, nthrusters+1):
# name = 'P' + str(i) + 'T1'
# exit = [0, r*np.cos((i-1)*(2 * np.pi / nthrusters)), r*np.sin((i-1)*(2 * np.pi / nthrusters))]
# config[name] = {'name': [name], 'type': ['001'], 'exit': [exit], 'dcm': dcm}
# thruster_groups['neg_x'].append(name)

# # define step sizes for each angle
# dcant = 10 # deg

# # create SweepAngles object
# angle_sweep = SweepConfig.SweepDecelAngles(config, thruster_groups)

# # call sweep_long_thruster on the configuration and print the DCM's
# config_swept_array = angle_sweep.sweep_decel_thrusters_all(dcant)

# for i, config in enumerate(config_swept_array):
# # Path to directory holding data assets and results for a specific RPOD study.
# case_dir = '../case/mdao/cant_sweep/'

# # Load JFH data.
# jfh = JetFiringHistory.JetFiringHistory(case_dir)
# jfh.read_jfh()

# tv = TargetVehicle.TargetVehicle(case_dir)
# tv.set_stl()

# vv = VisitingVehicle.VisitingVehicle(case_dir)
# vv.set_stl()
# vv.set_thruster_config(config)

# rpod = RPOD.RPOD(case_dir)
# rpod.study_init(jfh, tv, vv)

# rpod.visualize_sweep(i)
return

if __name__ == '__main__':
unittest.main()
1 change: 0 additions & 1 deletion tests/mdao/mdao_verification_test_04.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
# Initial variable sweep study used to explore the maximum overshoot velocity an LM can handle
# if the thruster configuration and decelaration starting distance are held constant.

import test_header
import unittest

from pyrpod.vehicle import TargetVehicle, LogisticsModule
Expand Down
1 change: 0 additions & 1 deletion tests/mdao/mdao_verification_test_05.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
# ========================
#

import test_header
import unittest

from pyrpod.vehicle import TargetVehicle, LogisticsModule
Expand Down
1 change: 0 additions & 1 deletion tests/mdao/mdao_verification_test_06.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
# ========================
#

import test_header
import unittest

from pyrpod.vehicle import TargetVehicle, LogisticsModule
Expand Down
1 change: 0 additions & 1 deletion tests/mission/mission_integration_test_01.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
# ========================
# A brief test case to calculate the 6DOF performance of each individual thruster in the LM

import test_header
import unittest, os, sys
import numpy as np

Expand Down
1 change: 0 additions & 1 deletion tests/mission/mission_integration_test_02.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
# ========================
# A brief test case to calculate the 6DOF performance of thruster working groups

import test_header
import unittest, os, sys
from pyrpod.vehicle import LogisticsModule

Expand Down
Loading
Loading