Skip to content

Commit

Permalink
added t-route unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
taddyb committed Dec 17, 2024
1 parent d0982a5 commit 5aecc16
Show file tree
Hide file tree
Showing 31 changed files with 1,889 additions and 1,375 deletions.
38 changes: 38 additions & 0 deletions doc/docker/dockerfile_notebook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Dockerfile.Notebook

This document describes the Docker setup for running JupyterLab with mounted volumes for development and analysis.

## Container Overview

The container provides a JupyterLab environment with:
- Python environment for data analysis
- Web interface accessible via port 8000

This container is a great way to run examples and integrated tests

## Docker Configuration

### Dockerfile
The Dockerfile sets up:
- Base Python environment
- JupyterLab installation
- Volume mount points for data and code
- Port 8000 exposed for web interface
- Working directory configuration

### Getting Started

Build:
```bash
docker build -t troute-notebook -f docker/Dockerfile.notebook .
```

Run:
```bash
docker run -p 8000:8000 troute-notebook
```

Then, take the URL from the output and put that into your browser. An example one is below:
```
http://127.0.0.1:8000/lab?token=<token>
```
29 changes: 29 additions & 0 deletions docker/Dockerfile.notebook
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
FROM rockylinux:9.2 as rocky-base
RUN yum install -y epel-release
RUN yum install -y netcdf netcdf-fortran netcdf-fortran-devel netcdf-openmpi

RUN yum install -y git cmake python python-devel pip

WORKDIR "/t-route/"

COPY . /t-route/

RUN ln -s /usr/lib64/gfortran/modules/netcdf.mod /usr/include/openmpi-x86_64/netcdf.mod

ENV VIRTUAL_ENV=/opt/venv
RUN python3 -m venv $VIRTUAL_ENV

# Equivalent to source /opt/venv/bin/activate
ENV PATH="$VIRTUAL_ENV/bin:$PATH"

RUN python -m pip install .
RUN python -m pip install .[jupyter]
RUN python -m pip install .[test]

RUN ./compiler.sh no-e

EXPOSE 8000

# increase max open files soft limit
RUN ulimit -n 10000
CMD ["jupyter", "lab", "--ip=0.0.0.0", "--port=8000", "--no-browser", "--allow-root"]
28 changes: 28 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[project]
name = "troute_project"
authors = [
{name = "DongHa Kim", email = "dongha.kim@noaa.gov"},
{name = "Sean Horvath", email = "sean.horvath@noaa.gov"},
{name = "Amin Torabi", email = "amin.torabi@noaa.gov"},
{name = "Zach Jurgen", email = "jurgen.zach@noaa.gov"},
{name = "Austin Raney", email = "austin.raney@noaa.gov"},
]
dynamic = ["version", "dependencies"]

[tool.setuptools.dynamic]
dependencies = {file = ["requirements.txt"]}

[project.optional-dependencies]
test = [
"pytest==8.3.2",
"bmipy==2.0.0",
]

jupyter = [
"contextily==1.6.0",
"matplotlib>=3.7.0,<3.8.0", # More stable version range
"ipykernel>=6.29.0,<7.0.0",
"jupyterlab>=3.6.7,<4.0.0",
"xarray>=2024.1.1",
"matplotlib-inline>=0.1.6" # Add explicit version
]
31 changes: 31 additions & 0 deletions src/troute-config/test/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import os

from contextlib import contextmanager
from pathlib import Path


@contextmanager
def temporarily_change_dir(path: Path):
"""Temporarily changes the current working directory
This context manager changes the current working directory to the specified path,
yields control back to the caller, and then changes back to the original directory
when exiting the context
Parameters
----------
path : Path
The path to temporarily change the current working directory to
Yields
------
None
"""
original_cwd = Path.cwd()
if original_cwd != path:
os.chdir(path)
try:
yield
finally:
if original_cwd != path:
os.chdir(original_cwd)
40 changes: 40 additions & 0 deletions src/troute-config/test/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from pathlib import Path
from typing import Any, Dict, List, Tuple

import pytest
import yaml
from _pytest.fixtures import FixtureRequest


def find_config_files() -> List[Path]:
"""Finds all `.yaml` configuration files within specified directories
Returns
-------
List[Path]
A list of Path objects pointing to each valid configuration
"""
test_dir = Path(__file__).parents[3] / "test" # Searching for the t-route/test dir
target_dirs = ["LowerColorado_TX", "LowerColorado_TX_v4", "LowerColorado_TX_HYFeatures_v22", "unit_test_hyfeature"]
files = []
for dir_name in target_dirs:
files.extend(list((test_dir / dir_name).glob("*.yaml")))
return files


@pytest.fixture(params=find_config_files())
def config_data(request: FixtureRequest) -> Tuple[Path, Dict[str, Any]]:
"""A fixture for loading yaml files into python dictionary mappings
Parameters
----------
request : FixtureRequest
The pytest request object, containing the current parameter value
Returns
-------
Tuple[Path, Dict[str, Any]]
A tuple containing the path to the YAML file and the loaded data as a dictionary
"""
data = yaml.load(request.param.read_text(), Loader=yaml.Loader)
return request.param, data
103 changes: 90 additions & 13 deletions src/troute-config/test/test_config.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,98 @@
import pytest

import yaml
from pathlib import Path
from typing import List
from test import temporarily_change_dir
from typing import Any, Dict, Tuple

import pytest
from pydantic import ValidationError
from troute.config import Config

TEST_DIR = Path(__file__).parent
ROOT_TEST_DIR = TEST_DIR / "../../../test"

def test_config_validation(config_data: Tuple[Path, Dict[str, Any]]) -> None:
"""Validates all config files contained within the `test/` folder
Parameters
----------
config_data : Tuple[Path, Dict[str, Any]]
A tuple containing the path to the config file and the parsed config data
- The first element is a Path object pointing to the config file
- The second element is a dictionary containing the parsed config yaml file data.
Raises
------
pytest.fail
If a ValidationError occurs during Config creation, this function will
call pytest.fail with a detailed error message showing the config file that fails
Notes
-----
This test function uses the `temporarily_change_dir` context manager to
change the working directory before attempting to create the Config object
"""
path, data = config_data
with temporarily_change_dir(path.parent):
try:
Config(**data)
except ValidationError as e:
error_details = "\n".join(
f"{' -> '.join(map(str, err['loc']))}: {err['msg']}"
for err in e.errors()
)
pytest.fail(f"Validation failed for {path}:\n{error_details}")


def test_strict_config_validation(config_data: Tuple[Path, Dict[str, Any]]) -> None:
"""Validates all config files contained within the `test/` folder via strict handling
Parameters
----------
config_data : Tuple[Path, Dict[str, Any]]
A tuple containing the path to the config file and the parsed config data
- The first element is a Path object pointing to the config file
- The second element is a dictionary containing the parsed config yaml file data.
Raises
------
pytest.fail
If a ValidationError occurs during Config creation, this function will
call pytest.fail with a detailed error message showing the config file that fails
def config_files() -> List[Path]:
files = list(ROOT_TEST_DIR.glob("*/*.yaml"))
return files
Notes
-----
- This test function uses the `temporarily_change_dir` context manager to
change the working directory before attempting to create the Config object
- If this code runs into a "value_error.path.not_exists" error, this is either because:
1. there is a relative path in the config
2. the file doesn't exist
Thus, we will make the relative path absolute and retry the validation. If that fails, we
know the file does not existt
"""
path, data = config_data
parent_path = path.parent
try:
with temporarily_change_dir(path.parent):
Config.with_strict_mode(**data)
except ValidationError as e:
for error in e.errors():
if error["type"] == "value_error.path.not_exists":
keys = error["loc"]
invalid_path = error["ctx"]["path"]
corrected_path = Path(parent_path, invalid_path).__str__()

# Ensuring the code path exists before changing relative path to absolute
if Path(corrected_path).exists():
current = data
for key in keys[:-1]:
current = current.setdefault(key, {})
current[keys[-1]] = corrected_path
else:
pytest.fail(f"Path does not exist: {corrected_path}")

@pytest.mark.parametrize("file", config_files())
def test_naive_deserialization(file: Path):
data = yaml.load(file.read_text(), Loader=yaml.Loader)
Config(**data)
try:
with temporarily_change_dir(path.parent):
Config.with_strict_mode(**data)
except ValidationError as e:
error_details = "\n".join(
f"{' -> '.join(map(str, err['loc']))}: {err['msg']}"
for err in e.errors()
)
pytest.fail(f"Validation failed for {path}:\n{error_details}")
1 change: 0 additions & 1 deletion src/troute-config/test/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import pytest
from pydantic import BaseModel, ValidationError

from troute.config._utils import use_strict
from troute.config.types import DirectoryPath, FilePath

Expand Down
31 changes: 31 additions & 0 deletions src/troute-network/test/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import os

from contextlib import contextmanager
from pathlib import Path


@contextmanager
def temporarily_change_dir(path: Path):
"""Temporarily changes the current working directory
This context manager changes the current working directory to the specified path,
yields control back to the caller, and then changes back to the original directory
when exiting the context
Parameters
----------
path : Path
The path to temporarily change the current working directory to
Yields
------
None
"""
original_cwd = Path.cwd()
if original_cwd != path:
os.chdir(path)
try:
yield
finally:
if original_cwd != path:
os.chdir(original_cwd)
Loading

0 comments on commit 5aecc16

Please sign in to comment.