Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a8007fd
Adding models and model fixtures
sankalps0549 Oct 25, 2025
2a5081f
MeshSummary Integration Tests
sankalps0549 Oct 25, 2025
0e69cce
Test Data Generation and Test fixes
sankalps0549 Oct 26, 2025
943899c
README update
sankalps0549 Oct 26, 2025
a72fead
README update
sankalps0549 Oct 26, 2025
fb13cbe
Comment fix
sankalps0549 Oct 26, 2025
1144fa6
Merge remote-tracking branch 'origin/mesh-summary-tests' into synergy…
sankalps0549 Oct 30, 2025
35e0d74
Add non-model based data generation
sankalps0549 Oct 30, 2025
d59d45d
Synergy Integration Tests
sankalps0549 Oct 31, 2025
ddd87bd
Adding pygetwindow to requirements
sankalps0549 Nov 1, 2025
ae09651
Merge remote-tracking branch 'origin/feature/integration-tests' into …
sankalps0549 Nov 3, 2025
55c897e
Windows size updates and Fixture updates
sankalps0549 Nov 4, 2025
c28e0ad
Copilot review fixes
sankalps0549 Nov 4, 2025
efe3c35
Zipping study_files
sankalps0549 Nov 5, 2025
f157c89
Zip File name changes
sankalps0549 Nov 6, 2025
71b713c
Metadata for Test data
sankalps0549 Nov 7, 2025
57eaad4
Unzip files in test data generation
sankalps0549 Nov 7, 2025
77de5ed
README updates
sankalps0549 Nov 7, 2025
e05096e
Review fixes
sankalps0549 Nov 7, 2025
b4b1d72
Removed ModelType enum
sankalps0549 Nov 9, 2025
8f3b23b
Review fixes
sankalps0549 Nov 9, 2025
b3fef79
Lint fixes
sankalps0549 Nov 9, 2025
1f16653
Project clean up
sankalps0549 Nov 10, 2025
df2cdfd
Revert "Project clean up"
sankalps0549 Nov 10, 2025
9828663
Remove edition assert
sankalps0549 Nov 12, 2025
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,7 @@ Thumbs.db

# Internal Autodesk directories
.adsk/

# Study Files folder - ignore everything except zip files
tests/api/integration_tests/study_files/*
!tests/api/integration_tests/study_files/*.zip
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pathspec==0.12.1
polib==1.2.0
pre-commit==4.2.0
pydata-sphinx-theme==0.16.1
pygetwindow==0.0.9
pylint==3.3.4
pytest==8.3.4
sphinx==8.1.3
Expand Down
458 changes: 262 additions & 196 deletions tests/api/integration_tests/README.md

Large diffs are not rendered by default.

132 changes: 86 additions & 46 deletions tests/api/integration_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,46 @@
import json
from pathlib import Path
import pytest
import tempfile
import zipfile
from moldflow import Synergy, Project, ItemType
from tests.api.integration_tests.constants import (
FileSet,
ModelType,
STUDY_FILES_DIR,
DATA_DIR,
DataFile,
DEFAULT_WINDOW_SIZE_X,
DEFAULT_WINDOW_SIZE_Y,
DEFAULT_WINDOW_POSITION_X,
DEFAULT_WINDOW_POSITION_Y,
PROJECT_ZIP_NAME_PATTERN,
PROJECT_PREFIX,
STUDY_FILE_EXTENSION,
PROJECT_EXTENSION,
)


def generate_file_map(
study_files_dir: str = STUDY_FILES_DIR,
) -> dict[FileSet, dict[ModelType, str]]:
def get_study_files():
"""
Dynamically generate the global file map for all file sets and model types.
Path pattern: {study_files_dir}/{fileset.value}/{modeltype.value}.sdy
Unzip the study files and return a dictionary of project names and corresponding study files.
Projects selected of the form project_<file_set>.
Study files selected of the form <model_name>.sdy.
"""
file_map = {}
for file_set in FileSet:
set_dir = Path(study_files_dir) / file_set.value
file_map[file_set.name] = {
model_type: str(set_dir / f"{model_type.value}.sdy") for model_type in ModelType
}
return file_map
for zip_file in STUDY_FILES_DIR.glob(PROJECT_ZIP_NAME_PATTERN):
with zipfile.ZipFile(zip_file, 'r') as zip_ref:
zip_ref.extractall(STUDY_FILES_DIR)

study_files = {}
for folders in STUDY_FILES_DIR.iterdir():
if folders.is_dir() and folders.name.startswith(PROJECT_PREFIX):
project_name = folders.name.replace(PROJECT_PREFIX, "")
study_files[project_name] = []
for model in folders.iterdir():
if model.name.endswith(STUDY_FILE_EXTENSION):
model_name = model.name.replace(STUDY_FILE_EXTENSION, "")
study_files[project_name].append(model_name)
return study_files

FILE_SETS = generate_file_map()

STUDY_FILES = get_study_files()


def pytest_generate_tests(metafunc):
Expand All @@ -61,11 +74,10 @@ def pytest_generate_tests(metafunc):
f"Test class '{metafunc_name}' requires a @pytest.mark.file_set(FileSet.<SET>) marker."
)

file_set = marker.args[0]
file_set_name = file_set.name
params = list(FILE_SETS[file_set_name].items())
ids = [f"{file_set}-{model_type.value}" for model_type, _ in params]

file_set = marker.args[0].value
study_files = STUDY_FILES[file_set]
params = study_files
ids = [f"{file_set}-{model}" for model in params]
metafunc.parametrize("study_file", params, ids=ids, scope="class")


Expand All @@ -74,9 +86,17 @@ def synergy_fixture():
"""
Fixture to create a real Synergy instance for integration testing.
"""
synergy_instance = Synergy()
synergy_instance = Synergy(logging=False)
synergy_instance.silence(True)
synergy_instance.set_application_window_pos(
DEFAULT_WINDOW_POSITION_X,
DEFAULT_WINDOW_POSITION_Y,
DEFAULT_WINDOW_SIZE_X,
DEFAULT_WINDOW_SIZE_Y,
)
yield synergy_instance
synergy_instance.quit(False)
if synergy_instance.synergy is not None:
synergy_instance.quit(False)


@pytest.fixture(scope="class", name="project")
Expand All @@ -90,9 +110,11 @@ def project_fixture(synergy: Synergy, request):
f"Test '{request.node.name}' requires a @pytest.mark.file_set(FileSet.<SET>) marker."
)

file_set = marker.args[0]
file_set = marker.args[0].value

project_path = Path(STUDY_FILES_DIR) / file_set.value / f"{file_set.value}.mpi"
project_path = (
Path(STUDY_FILES_DIR) / f"{PROJECT_PREFIX}{file_set}" / f"{file_set}{PROJECT_EXTENSION}"
)
project_handle = synergy.open_project(str(project_path))
if not project_handle:
raise RuntimeError(f"Failed to open project at {project_path}")
Expand All @@ -104,7 +126,7 @@ def project_fixture(synergy: Synergy, request):
@pytest.fixture(name="study_file")
def study_file_fixture(request):
"""
Provides a single (ModelType, file_path) tuple for each parametrized test.
Provides a single model_name string for each parametrized test.
"""
return request.param

Expand All @@ -114,50 +136,68 @@ def opened_study_fixture(project: Project, study_file):
"""
Opens a study file inside an already-open project.
"""
model_type, _ = study_file
study = project.open_item_by_name(model_type.value, ItemType.STUDY)
study = project.open_item_by_name(study_file, ItemType.STUDY)
return study


@pytest.fixture(name="study_with_project")
def study_with_project_fixture(project, study_file, opened_study):
"""
Provides (ModelType, file_path, project, opened_study) tuple for convenience.
Provides (model_name, project, opened_study) tuple for convenience.
"""
model_type, file_path = study_file
yield (model_type, file_path, project, opened_study)
yield (study_file, project, opened_study)


@pytest.fixture(scope="class", name="expected_data")
def expected_data_fixture(request):
"""
Load the expected data JSON file once per test class.

Expects the test class to define a class attribute:
`json_file_name = DataFile.MESH_SUMMARY` (for example)
Automatically derives the JSON filename from pytest markers.
Looks for markers like @pytest.mark.mesh_summary or @pytest.mark.synergy
and converts them to filenames like "mesh_summary_data.json" or "synergy_data.json".
"""
json_file_name = getattr(request.cls, "json_file_name", None)
if not json_file_name:
pytest.skip("Test class missing `json_file_name` attribute.")
# Try to derive filename from pytest markers first
json_file_name = None
marker_list = getattr(request.cls, "pytestmark", [])

# Look for data-related markers (excluding common ones like 'integration', 'file_set')
excluded_markers = {'integration', 'file_set', 'parametrize'}
for marker in marker_list:
if marker.name not in excluded_markers:
# Convert marker name to filename: mesh_summary -> mesh_summary_data.json
json_file_name = f"{marker.name}_data.json"
break

json_file = json_file_name.value if isinstance(json_file_name, DataFile) else json_file_name
json_path = Path(DATA_DIR) / json_file
json_path = Path(DATA_DIR) / json_file_name
if not json_path.exists():
pytest.skip(f"Expected data file not found: {json_path}")

with open(json_path, "r", encoding="utf-8") as f:
return json.load(f)
try:
with open(json_path, "r", encoding="utf-8") as f:
return json.load(f)
except json.JSONDecodeError:
pytest.skip(
f"Expected data file is not valid JSON: {json_path}. "
"Please run the data generation script to create/update the file."
)


@pytest.fixture(name="expected_values")
def expected_values_fixture(expected_data, study_file):
"""
Returns expected values for the current study's model type.
Returns expected values for the current study.

If no matching data exists, skips the test gracefully.
"""
model_type, _ = study_file
model_data = expected_data.get(model_type.value)
if not model_data:
pytest.skip(f"No expected values found for model type: {model_type.value}")
return model_data
expected_val = expected_data.get(study_file)
if not expected_val:
pytest.skip(f"No expected values found for model name: {study_file}")
return expected_val


@pytest.fixture(scope="class")
def temp_dir():
"""Create a temporary directory for integration testing."""
with tempfile.TemporaryDirectory() as tmpdir:
yield tmpdir
56 changes: 36 additions & 20 deletions tests/api/integration_tests/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,50 @@
STUDY_FILES_DIR = INTEGRATION_TESTS_DIR / "study_files"
DATA_DIR = INTEGRATION_TESTS_DIR / "data"

STUDIES_FILE_NAME = "studies.json"
STUDIES_FILE = Path(STUDY_FILES_DIR) / STUDIES_FILE_NAME

class DataFile(Enum):
"""
DataFile enum defines the different types of data files.
"""
METADATA_FILE_NAME = "metadata.json"
METADATA_FILE = Path(DATA_DIR) / METADATA_FILE_NAME

MESH_SUMMARY = "mesh_summary_data.json"
TEST_PROJECT_NAME = "test_project"

DEFAULT_WINDOW_SIZE_X = 2560
DEFAULT_WINDOW_SIZE_Y = 1440
DEFAULT_WINDOW_POSITION_X = 0
DEFAULT_WINDOW_POSITION_Y = 0

class FileSet(Enum):
"""
FileSet enum defines the different categories of study files.
SYNERGY_VERSION = "2026"
SYNERGY_WINDOW_TITLE = f"Autodesk Moldflow Insight {SYNERGY_VERSION}"

RAW: Unmeshed Unanalyzed Files
MESHED: Meshed Unanalyzed Files
ANALYZED: Meshed Analyzed Files
"""
METADATA_DATE_FORMAT = "%Y-%m-%d"
METADATA_TIME_FORMAT = "%H:%M:%S"

# RAW = "Raw"
MESHED = "Meshed"
# ANALYZED = "Analyzed"
TEMP_FILE_PREFIX = "temp_"
GENERATE_DATA_FUNCTION_PREFIX = "generate_"
GENERATE_DATA_FUNCTION_SUFFIX = "_data"

DATA_FILE_SUFFIX = "_data"
DATA_FILE_EXTENSION = ".json"

class ModelType(Enum):
PROJECT_PREFIX = "project_"
PROJECT_ZIP_NAME_PATTERN = f"{PROJECT_PREFIX}*.zip"
PROJECT_EXTENSION = ".mpi"

STUDY_FILE_EXTENSION = ".sdy"


class FileSet(Enum):
"""
ModelType enum defines the different types of models in each file set.
FileSet enum defines the different categories of study files.

SINGLE: Single Analyzed File for short tests
# RAW: Unmeshed Unanalyzed Files
MESHED: Meshed Unanalyzed Files
# ANALYZED: Meshed Analyzed Files
"""

DD = "dd_model"
MIDPLANE = "midplane_model"
THREE_D = "3d_model"
SINGLE = "single_study"
# RAW = "raw_studies"
MESHED = "meshed_studies"
# ANALYZED = "analyzed_studies"
63 changes: 41 additions & 22 deletions tests/api/integration_tests/data/data_generation/generate_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,25 @@
"""

import docopt
import logging
import sys
from datetime import datetime
from moldflow import Synergy
from tests.api.integration_tests.data.data_generation.generate_data_helper import generate_json
from tests.api.integration_tests.constants import FileSet, DataFile


@generate_json(json_file_name=DataFile.MESH_SUMMARY, file_set=FileSet.MESHED)
def generate_mesh_summary(synergy: Synergy = None):
from tests.api.integration_tests.data.data_generation.generate_data_helper import (
generate_json,
clean_up_temp_files,
get_generate_data_functions,
get_available_markers,
fetch_data_on_markers,
)
from tests.api.integration_tests.data.data_generation.generate_data_logger import (
generate_data_logger,
)
from tests.api.integration_tests.constants import FileSet
from tests.api.integration_tests.conftest import get_study_files


@generate_json(file_set=FileSet.MESHED)
def generate_mesh_summary_data(synergy: Synergy = None):
"""
Extract mesh summary data from a study.
Returns a dict with relevant properties.
Expand Down Expand Up @@ -57,34 +67,43 @@ def generate_mesh_summary(synergy: Synergy = None):
}


GENERATE_FUNCTIONS = {"mesh_summary": generate_mesh_summary}
@generate_json(file_set=None)
def generate_synergy_data(synergy: Synergy = None):
"""
Generate data for the Synergy class.
Returns a dict with relevant properties.
"""

build_number_parts = synergy.build_number.split(".")
build_number_major_minor = ".".join(build_number_parts[:2])

return {"version": synergy.version, "build_number": build_number_major_minor}


def main():
"""Main entry point for this script"""
args = docopt.docopt(__doc__)
DATE_TIME = datetime.now()

try:
markers = args.get('<markers>') or []
if len(markers) > 0:

for marker in markers:
generate_function = GENERATE_FUNCTIONS.get(marker)
get_study_files()
generate_functions = get_generate_data_functions(globals())

if generate_function:
generate_function()
else:
logging.error('FAILURE: No generate function found for marker: %s', marker)
return 1
for marker in markers:
if marker not in generate_functions.keys():
generate_data_logger.error(f'Invalid marker: {marker}')
generate_data_logger.error(get_available_markers(generate_functions))
return 0

if len(markers) > 0:
fetch_data_on_markers(markers, generate_functions, DATE_TIME)
else:
logging.info('Generating all data')

for generate_function in GENERATE_FUNCTIONS.values():
generate_function()
fetch_data_on_markers(generate_functions.keys(), generate_functions, DATE_TIME)

except Exception as err:
logging.error('FAILURE: %s', err, exc_info=True)
generate_data_logger.error(f'FAILURE: {err}')
clean_up_temp_files()
return 1

return 0
Expand Down
Loading