Source code for turbo_turtle._tests.test_system

"""System test wrapper for executing shell commands with pytest results reporting.

All tests should be marked ``pytest.mark.systemtest`` (handled in the test function markers).

All tests that require a third-party software unavailable on conda-forge should be marked as
``pytest.mark.require_third_party``.

All tests should use string template substitution instead of f-strings, if possible. See :meth:`test_system` for
available substitutions.
"""

import os
import pathlib
import shlex
import string
import subprocess
import tempfile
import typing
from importlib.metadata import PackageNotFoundError, version

import pytest

from turbo_turtle import _settings, _utilities
from turbo_turtle._main import get_parser
from turbo_turtle.conftest import missing_display

parser = get_parser()
subcommand_list = parser._subparsers._group_actions[0].choices.keys()
env = os.environ.copy()
turbo_turtle_command = "turbo-turtle"

try:
    version("turbo_turtle")
    installed = True
except PackageNotFoundError:
    # TODO: Recover from the SCons task definition?
    build_directory = _settings._project_root_abspath.parent / "build" / "systemtests"
    build_directory.mkdir(parents=True, exist_ok=True)
    installed = False

# If executing in repository, add package to PYTHONPATH and change the root command
if not installed:
    turbo_turtle_command = "python -m turbo_turtle._main"
    package_parent_path = _settings._project_root_abspath.parent
    key = "PYTHONPATH"
    if key in env:
        env[key] = f"{package_parent_path}:{env[key]}"
    else:
        env[key] = f"{package_parent_path}"


[docs] def setup_sphere_commands( model: str, inner_radius: float, outer_radius: float, angle: float, y_offset: float, quadrant: str, element_type: str, element_replacement: str, backend: str, output_type: str, ) -> list[string.Template]: """Return the sphere/partition/mesh commands for system testing. :returns: list of string or string template commands """ model = pathlib.Path(model).with_suffix(".cae") image = model.with_suffix(".png") # TODO: Merge Gmsh sphere system tests when partition/mesh/image subcommands are implemented # https://re-git.lanl.gov/aea/python-projects/turbo-turtle/-/issues/217 if backend == "cubit": model = model.with_suffix(".cub") image = image.parent / f"{image.stem}-cubit{image.suffix}" assembly = model.stem + "_assembly.inp" center = f"0. {y_offset} 0." xvector = _utilities.character_delimited_list([1.0, 0.0, 0.0]) zvector = _utilities.character_delimited_list([0.0, 0.0, 1.0]) backend_option = f"--backend {backend}" if backend is not None else "" commands = [ string.Template( "${turbo_turtle_command} sphere --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " f"--inner-radius {inner_radius} --outer-radius {outer_radius} --output-file {model} " f"--model-name {model.stem} --part-name {model.stem} --quadrant {quadrant} " f"--revolution-angle {angle} --y-offset {y_offset} {backend_option}" ), string.Template( "${turbo_turtle_command} partition --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " f"--input-file {model} --output-file {model} " f"--model-name {model.stem} --part-name {model.stem} --center {center} " f"--xvector {xvector} --zvector {zvector} {backend_option}" ), string.Template( "${turbo_turtle_command} mesh --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " f"--input-file {model} --output-file {model} " f"--model-name {model.stem} --part-name {model.stem} --global-seed 0.15 " f"--element-type {element_type} {backend_option}" ), string.Template( "${turbo_turtle_command} image --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " f"--input-file {model} --output-file {image} " f"--model-name {model.stem} --part-name {model.stem} {backend_option}" ), string.Template( "${turbo_turtle_command} export --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " f"--input-file {model} --model-name {model.stem} --part-name {model.stem} " f"--element-type {element_replacement} --destination . " f"--assembly {assembly} --output-type {output_type} {backend_option}" ), ] # Skip the image subcommand when DISPLAY is not found # Skip the image subcommand when running the Genesis variations. We don't need duplicate images of the cubit meshes if (backend == "cubit" and missing_display) or (backend == "cubit" and output_type.lower() == "genesis"): commands.pop(3) # Skip the partition/mesh/image/export if inner_radius == 0: commands = [commands[0]] return pytest.param(commands, marks=getattr(pytest.mark, backend))
[docs] def setup_geometry_xyplot_commands(model: str, input_file: list[pathlib.Path], backend: str) -> list[string.Template]: """Return system test geometry xyplot commands.""" part_name = _utilities.character_delimited_list(csv.stem for csv in input_file) input_file = _utilities.character_delimited_list(input_file) commands = [ string.Template( "${turbo_turtle_command} geometry-xyplot " "--abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " f"--input-file {input_file} --output-file {model}.png --part-name {part_name} " f"--backend {backend}" ) ] return pytest.param(commands, marks=getattr(pytest.mark, backend))
[docs] def setup_geometry_commands( model: str, input_file: list[pathlib.Path], revolution_angle: float, y_offset: float, backend: str, ) -> list[string.Template]: """Return system test geometry commands.""" model = pathlib.Path(model).with_suffix(".cae") if backend == "cubit": model = model.with_suffix(".cub") if backend == "gmsh": model = model.with_suffix(".step") part_name = " ".join(csv.stem for csv in input_file) input_file = _utilities.character_delimited_list(input_file) backend_option = f"--backend {backend}" if backend is not None else "" commands = [ string.Template( "${turbo_turtle_command} geometry --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " f"--input-file {input_file} --model-name {model.stem} " f"--part-name {part_name} --output-file {model} --revolution-angle {revolution_angle} " f"--y-offset {y_offset} {backend_option}" ) ] return pytest.param(commands, marks=getattr(pytest.mark, backend))
[docs] def setup_sets_commands( model: str, input_file: pathlib.Path, revolution_angle: float, face_sets: typing.Sequence[tuple[str, str]] | None, edge_sets: typing.Sequence[tuple[str, str]] | None, edge_seeds: typing.Sequence[tuple[str, str]] | None, element_type: str, backend: str, ) -> list[string.Template]: """Return system test sets commands.""" model = pathlib.Path(model).with_suffix(".cae") if backend == "cubit": model = model.with_suffix(".cub") part_name = " ".join(csv.stem for csv in input_file) commands = setup_geometry_commands(model, input_file, revolution_angle, 0.0, backend).values[0] if face_sets is not None: face_sets = _utilities.construct_append_options("--face-set", face_sets) else: face_sets = "" if edge_sets is not None: edge_sets = _utilities.construct_append_options("--edge-set", edge_sets) else: edge_sets = "" if edge_seeds is not None: edge_seeds = _utilities.construct_append_options("--edge-seed", edge_seeds) else: edge_seeds = "" backend_option = f"--backend {backend}" if backend is not None else "" sets_commands = [ string.Template( "${turbo_turtle_command} sets --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " f"--input-file {model} --model-name {model.stem} " f"--part-name {part_name} --output-file {model} {face_sets} {edge_sets} {backend_option}" ), string.Template( "${turbo_turtle_command} mesh --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " f"--input-file {model} --model-name {model.stem} " f"--part-name {part_name} --output-file {model} --global-seed 1. --element-type {element_type} " f"{edge_seeds} {backend_option}" ), ] commands.extend(sets_commands) return pytest.param(commands, marks=getattr(pytest.mark, backend))
[docs] def setup_cylinder_commands(model: str, revolution_angle: float, backend: str) -> list[string.Template]: """Return system test cylinder commands.""" model = pathlib.Path(model).with_suffix(".cae") if backend == "cubit": model = model.with_suffix(".cub") if backend == "gmsh": model = model.with_suffix(".step") backend_option = f"--backend {backend}" if backend is not None else "" commands = [ string.Template( "${turbo_turtle_command} cylinder --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " f"--model-name {model.stem} --part-name {model.stem} " f"--output-file {model} --revolution-angle {revolution_angle} " f"--inner-radius 1 --outer-radius 2 --height 1 {backend_option}" ) ] return pytest.param(commands, marks=getattr(pytest.mark, backend))
[docs] def setup_merge_commands(part_name: str, backend: str) -> list[string.Template]: """Return system test merge commands.""" commands = [] sphere_model = pathlib.Path("merge-sphere.cae") sphere_element_type = "C3D8" sphere_element_replacement = "C3D8R" geometry_model = pathlib.Path("merge-multi-part") output_file = pathlib.Path("merge.cae") if backend == "cubit": sphere_model = sphere_model.with_suffix(".cub") sphere_element_type = None sphere_element_replacement = "HEX20" geometry_model = geometry_model.with_suffix(".cub") output_file = output_file.with_suffix(".cub") # Create sphere file sphere_options = ( str(sphere_model), 1.0, 2.0, 360.0, 0.0, "both", sphere_element_type, sphere_element_replacement, backend, "abaqus", ) commands.append(setup_sphere_commands(*sphere_options).values[0][0]) # Create washer/vase combined file geometry_options = ( str(geometry_model), [ _settings._project_root_abspath / "_tests" / "washer.csv", _settings._project_root_abspath / "_tests" / "vase.csv", ], 360.0, 0.0, backend, ) commands.extend(setup_geometry_commands(*geometry_options).values[0]) # Run the actual merge command part_name = f"--part-name {part_name}" if part_name else "" backend_option = f"--backend {backend}" if backend is not None else "" merge_command = string.Template( "${turbo_turtle_command} merge --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " f"--input-file {sphere_model} {geometry_model} " f"--output-file {output_file} --merged-model-name merge " f"--model-name merge-multi-part merge-sphere {part_name} {backend_option}" ) commands.append(merge_command) return pytest.param(commands, marks=getattr(pytest.mark, backend))
commands_list = [] # Legacy geometry system tests requires a series of commands before the temp directory is removed # TODO: Decide if we should package or drop the legacy geometry tests name = "Turbo-Turtle-Tests" legacy_geometry_file = _settings._project_root_abspath / "_tests" / "legacy_geometry.py" commands_list.append( pytest.param( [ string.Template(f"${{abaqus_command}} cae -noGui {legacy_geometry_file}"), string.Template( "${turbo_turtle_command} partition --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " f"--input-file {name}.cae --output-file {name}.cae --model-name {name} " f"--part-name seveneigths-sphere --center 0 0 0 --xvector 1 0 0 --zvector 0 0 1" ), string.Template( "${turbo_turtle_command} image --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " f"--input-file {name}.cae --model-name {name} " f"--output-file seveneigths-sphere.png --part-name seveneigths-sphere" ), string.Template( "${turbo_turtle_command} partition --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " f"--input-file {name}.cae --output-file {name}.cae --model-name {name} --part-name swiss-cheese " "--center 0 0 0 --xvector 1 0 0 --zvector 0 0 1" ), string.Template( "${turbo_turtle_command} image --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " f"--input-file {name}.cae --model-name {name} --output-file swiss-cheese.png --part-name swiss-cheese" ), ], marks=pytest.mark.abaqus, ) ) # Sphere/partition/mesh sphere_tests = ( # Arguments: # model/part, inner_radius, outer_radius, angle, y-offset, # quadrant, element_type, element_replacement, backend, output_type # Abaqus CAE ("sphere.cae", 1.0, 2.0, 360.0, 0.0, "both", "C3D8", "C3D8R", "abaqus", "abaqus"), ("solid-sphere.cae", 0.0, 2.0, 360.0, 0.0, "both", "C3D8", "C3D8R", "abaqus", "abaqus"), ("axisymmetric.cae", 1.0, 2.0, 0.0, 0.0, "both", "CAX4", "CAX4R", "abaqus", "abaqus"), ("quarter-sphere.cae", 1.0, 2.0, 90.0, 0.0, "both", "C3D8", "C3D8R", "abaqus", "abaqus"), ("offset-sphere.cae", 1.0, 2.0, 360.0, 1.0, "both", "C3D8", "C3D8R", "abaqus", "abaqus"), ("eigth-sphere.cae", 1.0, 2.0, 90.0, 0.0, "upper", "C3D8", "C3D8R", "abaqus", "abaqus"), ("half-sphere.cae", 1.0, 2.0, 360.0, 0.0, "upper", "C3D8", "C3D8R", "abaqus", "abaqus"), # Cubit: for Abaqus INP ("sphere.cae", 1.0, 2.0, 360.0, 0.0, "both", None, "C3D8R", "cubit", "abaqus"), ("solid-sphere.cae", 0.0, 2.0, 360.0, 0.0, "both", None, "C3D8R", "cubit", "abaqus"), ("axisymmetric.cae", 1.0, 2.0, 0.0, 0.0, "both", None, "CAX4R", "cubit", "abaqus"), ("quarter-sphere.cae", 1.0, 2.0, 90.0, 0.0, "both", None, "C3D8R", "cubit", "abaqus"), ("offset-sphere.cae", 1.0, 2.0, 360.0, 1.0, "both", None, "C3D8R", "cubit", "abaqus"), ("eigth-sphere.cae", 1.0, 2.0, 90.0, 0.0, "upper", None, "C3D8R", "cubit", "abaqus"), ("half-sphere.cae", 1.0, 2.0, 360.0, 0.0, "upper", None, "C3D8R", "cubit", "abaqus"), # Cubit "element type" is really a "meshing scheme" ("sphere-tets.cae", 1.0, 2.0, 360.0, 0.0, "both", "tetmesh", None, "cubit", "abaqus"), ("axisymmetric-tri.cae", 1.0, 2.0, 0.0, 0.0, "both", "trimesh", None, "cubit", "abaqus"), # Cubit: for Genesis INP ("sphere-genesis.cae", 1.0, 2.0, 360.0, 0.0, "both", None, "HEX", "cubit", "genesis"), ( "solid-sphere-genesis.cae", 0.0, 2.0, 360.0, 0.0, "both", None, "HEX", "cubit", "genesis", ), ( "axisymmetric-genesis.cae", 1.0, 2.0, 0.0, 0.0, "both", None, "QUAD", "cubit", "genesis", ), ( "quarter-sphere-genesis.cae", 1.0, 2.0, 90.0, 0.0, "both", None, "HEX", "cubit", "genesis", ), ( "offset-sphere-genesis.cae", 1.0, 2.0, 360.0, 1.0, "both", None, "HEX", "cubit", "genesis", ), ( "eigth-sphere-genesis.cae", 1.0, 2.0, 90.0, 0.0, "upper", None, "HEX", "cubit", "genesis", ), ( "half-sphere-genesis.cae", 1.0, 2.0, 360.0, 0.0, "upper", None, "HEX", "cubit", "genesis", ), # Cubit "element type" is really a "meshing scheme" ( "sphere-tets-genesis.cae", 1.0, 2.0, 360.0, 0.0, "both", "tetmesh", "TRI", "cubit", "genesis", ), ( "axisymmetric-tri-genesis.cae", 1.0, 2.0, 0.0, 0.0, "both", "trimesh", "TETRA", "cubit", "genesis", ), ) commands_list.extend([setup_sphere_commands(*test) for test in sphere_tests]) # Geometry XY Plot tests geometry_xyplot_tests = ( # model/part, input_file, backend ("washer", [_settings._project_root_abspath / "_tests" / "washer.csv"], "abaqus"), ("vase", [_settings._project_root_abspath / "_tests" / "vase.csv"], "abaqus"), ( "multi-part", [ _settings._project_root_abspath / "_tests" / "washer.csv", _settings._project_root_abspath / "_tests" / "vase.csv", ], "abaqus", ), ) commands_list.extend([setup_geometry_xyplot_commands(*test) for test in geometry_xyplot_tests]) # Geometry tests geometry_tests = ( # model/part, input_file, angle, y-offset, backend # Abaqus ( "washer", [_settings._project_root_abspath / "_tests" / "washer.csv"], 360.0, 0.0, "abaqus", ), ( "offset-washer", [_settings._project_root_abspath / "_tests" / "washer.csv"], 360.0, 1.0, "abaqus", ), ( "washer-axisymmetric", [_settings._project_root_abspath / "_tests" / "washer.csv"], 0.0, 0.0, "abaqus", ), ("vase", [_settings._project_root_abspath / "_tests" / "vase.csv"], 360.0, 0.0, "abaqus"), ( "vase-axisymmetric", [_settings._project_root_abspath / "_tests" / "vase.csv"], 0.0, 0.0, "abaqus", ), ( "multi-part-3D", [ _settings._project_root_abspath / "_tests" / "washer.csv", _settings._project_root_abspath / "_tests" / "vase.csv", ], 360.0, 0.0, "abaqus", ), ( "multi-part-2D", [ _settings._project_root_abspath / "_tests" / "washer.csv", _settings._project_root_abspath / "_tests" / "vase.csv", ], 0.0, 0.0, "abaqus", ), # Cubit ( "washer", [_settings._project_root_abspath / "_tests" / "washer.csv"], 360.0, 0.0, "cubit", ), ( "offset-washer", [_settings._project_root_abspath / "_tests" / "washer.csv"], 360.0, 1.0, "cubit", ), ( "washer-axisymmetric", [_settings._project_root_abspath / "_tests" / "washer.csv"], 0.0, 0.0, "cubit", ), ("vase", [_settings._project_root_abspath / "_tests" / "vase.csv"], 360.0, 0.0, "cubit"), ( "vase-axisymmetric", [_settings._project_root_abspath / "_tests" / "vase.csv"], 0.0, 0.0, "cubit", ), ( "multi-part-3D", [ _settings._project_root_abspath / "_tests" / "washer.csv", _settings._project_root_abspath / "_tests" / "vase.csv", ], 360.0, 0.0, "cubit", ), ( "multi-part-2D", [ _settings._project_root_abspath / "_tests" / "washer.csv", _settings._project_root_abspath / "_tests" / "vase.csv", ], 0.0, 0.0, "cubit", ), # Gmsh ( "washer", [_settings._project_root_abspath / "_tests" / "washer.csv"], 360.0, 0.0, "gmsh", ), ( "offset-washer", [_settings._project_root_abspath / "_tests" / "washer.csv"], 360.0, 1.0, "gmsh", ), ( "washer-axisymmetric", [_settings._project_root_abspath / "_tests" / "washer.csv"], 0.0, 0.0, "gmsh", ), ("vase", [_settings._project_root_abspath / "_tests" / "vase.csv"], 360.0, 0.0, "gmsh"), ( "vase-axisymmetric", [_settings._project_root_abspath / "_tests" / "vase.csv"], 0.0, 0.0, "gmsh", ), ( "multi-part-3D", [ _settings._project_root_abspath / "_tests" / "washer.csv", _settings._project_root_abspath / "_tests" / "vase.csv", ], 360.0, 0.0, "gmsh", ), ( "multi-part-2D", [ _settings._project_root_abspath / "_tests" / "washer.csv", _settings._project_root_abspath / "_tests" / "vase.csv", ], 0.0, 0.0, "gmsh", ), ) commands_list.extend([setup_geometry_commands(*test) for test in geometry_tests]) # Sets/mesh tests sets_tests = ( # model/part, input_file, angle, face_sets, edge_sets, edge seeds, element_type, backend # Abaqus # TODO: Pick some Abaqus edge sets for the system tests ( "vase", [_settings._project_root_abspath / "_tests" / "vase.csv"], 360.0, [["top", "'[#4 ]'"], ["bottom", "'[#40 ]'"]], None, None, "C3D8R", "abaqus", ), ( "vase-axisymmetric", [_settings._project_root_abspath / "_tests" / "vase.csv"], 0.0, None, [["top", "'[#10 ]'"], ["bottom", "'[#1 ]'"]], [["top", "5"], ["bottom", "0.5"]], "CAX4", "abaqus", ), # Cubit ( "vase", [_settings._project_root_abspath / "_tests" / "vase.csv"], 360.0, [["top", "4"], ["bottom", "7"], ["outer", "'2 3 8'"]], [["top_outer", "13"]], [["top_outer", "1.000001"]], "C3D8R", "cubit", ), ( "vase-axisymmetric", [_settings._project_root_abspath / "_tests" / "vase.csv"], 0.0, None, [["top", "2"], ["bottom", "6"], ["outer", "'1 7 8'"]], [["top", "1.000001"]], "CAX4", "cubit", ), ) commands_list.extend([setup_sets_commands(*test) for test in sets_tests]) # Cylinder tests cylinder_tests = ( # model/part, angle, backend ("cylinder_3d", 360.0, "abaqus"), ("cylinder_2d", 0.0, "abaqus"), ("cylinder_3d", 360.0, "cubit"), ("cylinder_2d", 0.0, "cubit"), ("cylinder_3d", 360.0, "gmsh"), ("cylinder_2d", 0.0, "gmsh"), ) commands_list.extend([setup_cylinder_commands(*test) for test in cylinder_tests]) # TODO: Merge Gmsh sphere system tests when partition/mesh/image subcommands are implemented # https://re-git.lanl.gov/aea/python-projects/turbo-turtle/-/issues/217 gmsh_sphere_2D = [ # noqa: N816 [ string.Template( "${turbo_turtle_command} sphere --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " "--inner-radius 1. --outer-radius 2. --output-file sphere.step --revolution-angle=0. --backend gmsh" ) ], [ string.Template( "${turbo_turtle_command} sphere --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " "--inner-radius 1. --outer-radius 2. --output-file sphere.step --revolution-angle=0. " "--backend gmsh --quadrant upper" ), ], [ string.Template( "${turbo_turtle_command} sphere --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " "--inner-radius 1. --outer-radius 2. --output-file sphere.step --revolution-angle=0. " "--backend gmsh --quadrant lower" ), ], [ string.Template( "${turbo_turtle_command} sphere --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " "--inner-radius 0. --outer-radius 1. --output-file sphere.step --revolution-angle=0. --backend gmsh" ), ], [ string.Template( "${turbo_turtle_command} sphere --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " "--inner-radius 0. --outer-radius 1. --output-file sphere.step --revolution-angle=0. " "--backend gmsh --quadrant upper" ), ], [ string.Template( "${turbo_turtle_command} sphere --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " "--inner-radius 0. --outer-radius 1. --output-file sphere.step --revolution-angle=0. " "--backend gmsh --quadrant lower" ), ], ] for test in gmsh_sphere_2D: test.append( string.Template( "${turbo_turtle_command} mesh --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " "--input-file sphere.step --output-file sphere.msh --global-seed 1. --element-type unused " "--backend gmsh" ) ) if not missing_display: test.append( string.Template( "${turbo_turtle_command} image --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " "--input-file sphere.step --output-file sphere.step.png --x-angle 0 --y-angle 0 --backend gmsh" ) ) test.append( string.Template( "${turbo_turtle_command} image --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " "--input-file sphere.msh --output-file sphere.msh.png --x-angle 0 --y-angle 0 --backend gmsh" ) ) commands_list.append(pytest.param(test, marks=pytest.mark.gmsh)) gmsh_sphere_3D = [ # noqa: N816 [ string.Template( "${turbo_turtle_command} sphere --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " "--inner-radius 1. --outer-radius 2. --output-file sphere.step --revolution-angle=360. " "--backend gmsh" ), ], [ string.Template( "${turbo_turtle_command} sphere --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " "--inner-radius 1. --outer-radius 2. --output-file sphere.step --revolution-angle=360. " "--backend gmsh --quadrant upper" ), ], [ string.Template( "${turbo_turtle_command} sphere --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " "--inner-radius 1. --outer-radius 2. --output-file sphere.step --revolution-angle=360. " "--backend gmsh --quadrant lower" ), ], # TODO: Fix solid sphere revolve # https://re-git.lanl.gov/aea/python-projects/turbo-turtle/-/issues/218 # Undo changes in commit 72579ae84a82071cee349b05046cabb26f835f55 [ string.Template( "${turbo_turtle_command} sphere --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " "--inner-radius 0. --outer-radius 1. --output-file sphere.step --revolution-angle=360. " "--backend gmsh --quadrant upper" ) ], [ string.Template( "${turbo_turtle_command} sphere --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " "--inner-radius 0. --outer-radius 1. --output-file sphere.step --revolution-angle=360. " "--backend gmsh --quadrant lower" ) ], ] for test in gmsh_sphere_3D: test.append( string.Template( "${turbo_turtle_command} mesh --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " "--input-file sphere.step --output-file sphere.msh --global-seed 1. --element-type unused " "--backend gmsh" ) ) if not missing_display: test.append( string.Template( "${turbo_turtle_command} image --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " "--input-file sphere.step --output-file sphere.step.png --x-angle 45.0 --y-angle -45.0 " "--backend gmsh" ) ) test.append( string.Template( "${turbo_turtle_command} image --abaqus-command ${abaqus_command} --cubit-command ${cubit_command} " "--input-file sphere.msh --output-file sphere.msh.png --x-angle 45.0 --y-angle -45.0 --backend gmsh" ) ) commands_list.append(pytest.param(test, marks=pytest.mark.gmsh)) # Merge tests for part_name in ("washer vase merge-sphere", ""): commands_list.append(setup_merge_commands(part_name, backend="abaqus")) commands_list.append(setup_merge_commands(part_name, backend="cubit")) # SCons extensions tests # System tests as SCons tasks # TODO: Decide how to handle this system test which requires both Abaqus and Cubit. # Separate the SConstruct file into Abaqus/Cubit halves? Dedicated, non-matrixed construction environment? sconstruct = _settings._project_root_abspath / "_tests/SConstruct" commands_list.append( pytest.param( [ string.Template( f"scons . --sconstruct={sconstruct} --turbo-turtle-command='${{turbo_turtle_command}}' " "--abaqus-command=${abaqus_command} --backend=abaqus" ) ], marks=pytest.mark.abaqus, ) ) commands_list.append( pytest.param( [ string.Template( f"scons . --sconstruct={sconstruct} --turbo-turtle-command='${{turbo_turtle_command}}' " "--cubit-command=${cubit_command} --backend=cubit" ) ], marks=pytest.mark.cubit, ) ) # User manual example SCons tasks sconstruct_files = [ [ _settings._project_root_abspath / "tutorials/SConstruct", _settings._project_root_abspath / "tutorials/SConscript", ] ] for files in sconstruct_files: space_delimited_files = " ".join([str(path) for path in files]) scons_test_commands = [ string.Template("${turbo_turtle_command} fetch SConstruct SConscript"), # FIXME: Figure out why this command fails on the CI server, but not in local user tests # https://re-git.lanl.gov/aea/python-projects/turbo-turtle/-/issues/159 # f"scons . --turbo-turtle-command="{turbo_turtle_command}"" ] commands_list.append(scons_test_commands)
[docs] @pytest.mark.systemtest @pytest.mark.require_third_party @pytest.mark.parametrize("commands", commands_list) def test_require_third_party( abaqus_command: pathlib.Path, cubit_command: pathlib.Path, commands: list[str | string.Template] ) -> None: """Run system tests that require third-party software. Executes with a temporary directory that is cleaned up after each test execution. Accepts custom pytest CLI options to specify abaqus and cubit commands .. code-block:: pytest --abaqus-command /my/system/abaqus --cubit-command /my/system/cubit :param abaqus_command: string absolute path to Abaqus executable :param cubit_command: string absolute path to Cubit executable :param command: the full list of command string(s) for the system test """ test_project_shell_commands(abaqus_command, cubit_command, commands)
[docs] def run_commands( commands: list[str | string.Template], build_directory: str | pathlib.Path | tempfile.TemporaryDirectory, template_substitution: dict | None = None, ) -> None: """Run shell commands with a dedicated test shell environment in a temporary working directory.""" if template_substitution is None: template_substitution = {} for command in commands: if isinstance(command, string.Template): command_string = command.substitute(template_substitution) else: command_string = command command_list = shlex.split(command_string) subprocess.check_output(command_list, env=env, cwd=build_directory).decode("utf-8")
# Help/Usage sign-of-life project_only_commands_list = [string.Template("${turbo_turtle_command} -h")] project_only_commands_list.extend( [string.Template(f"${{turbo_turtle_command}} {subcommand} -h") for subcommand in subcommand_list] ) project_only_commands_list.append(string.Template("${turbo_turtle_command} fetch"))
[docs] @pytest.mark.systemtest @pytest.mark.parametrize("commands", project_only_commands_list) def test_project_shell_commands( abaqus_command: pathlib.Path, cubit_command: pathlib.Path, commands: list[str | string.Template] ) -> None: """Run the system tests. Executes with a temporary directory that is cleaned up after each test execution. Accepts custom pytest CLI options to specify abaqus and cubit commands .. code-block:: pytest --abaqus-command /my/system/abaqus --cubit-command /my/system/cubit :param abaqus_command: string absolute path to Abaqus executable :param cubit_command: string absolute path to Cubit executable :param command: the full list of command string(s) for the system test """ template_substitution = { "turbo_turtle_command": turbo_turtle_command, "abaqus_command": abaqus_command, "cubit_command": cubit_command, } if isinstance(commands, str) or isinstance(commands, string.Template): commands = [commands] if installed: with tempfile.TemporaryDirectory() as temp_directory: run_commands(commands, temp_directory, template_substitution=template_substitution) else: with tempfile.TemporaryDirectory(dir=build_directory, ignore_cleanup_errors=True) as temp_directory: run_commands(commands, temp_directory, template_substitution=template_substitution)