Skip to content

Commit

Permalink
[CI] Add workflow for pytest-blender unit tests (#3)
Browse files Browse the repository at this point in the history
* Add some very simple unit tests

* [CI] Add workflow for pytest-blender

* Add some unit tests for math.py
  • Loading branch information
Lathreas committed Dec 30, 2022
1 parent 692d1ca commit f4dd8c5
Show file tree
Hide file tree
Showing 4 changed files with 260 additions and 0 deletions.
60 changes: 60 additions & 0 deletions .github/workflows/pytest-blender.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: pytest-blender

on:
push:
branches:
- main
- geoscript-pytest-configuration
pull_request:
branches:
- main
- geoscript-pytest-mypy-ci

jobs:
test:
name: Test with Blender ${{ matrix.blender-version }} on ${{ matrix.platform }}
runs-on: ${{ matrix.platform }}
strategy:
matrix:
platform:
- ubuntu-latest
- macOS-latest
blender-version:
- '3.3.0'
- '3.3.1'
steps:
- uses: actions/checkout@v3
- name: Set up Python v3.10
uses: actions/setup-python@v3
with:
python-version: "3.10"
- name: Upgrade PIP
run: python -m pip install --upgrade pip
- name: Cache Blender ${{ matrix.blender-version }}
uses: actions/cache@v3
id: cache-blender
with:
path: |
~/blender-*
~/_blender-executable-path.txt
key: ${{ runner.os }}-${{ matrix.blender-version }}
- name: Download Blender ${{ matrix.blender-version }}
if: steps.cache-blender.outputs.cache-hit != 'true'
id: download-blender
run: |
python -m pip install --upgrade blender-downloader
printf "%s" "$(blender-downloader \
${{ matrix.blender-version }} --extract --remove-compressed \
--output-directory ~/ \
--quiet --print-blender-executable)" > ~/_blender-executable-path.txt
- name: Install dependencies
id: install-dependencies
run: |
python -m pip install pytest-blender pytest
blender_executable="$(< ~/_blender-executable-path.txt)"
python_blender_executable="$(pytest-blender --blender-executable $blender_executable)"
$python_blender_executable -m ensurepip
$python_blender_executable -m pip install pytest
echo "blender-executable=$blender_executable" >> $GITHUB_OUTPUT
- name: Test with pytest
run: pytest -svv --blender-executable "${{ steps.install-dependencies.outputs.blender-executable }}"
6 changes: 6 additions & 0 deletions modules/geoscript/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/python3

"""This package contains unit tests for Geoscript"""

# This file intentionally contains no code. See the other .py files in this package
# for the specific unit tests.
134 changes: 134 additions & 0 deletions modules/geoscript/tests/test_math.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
#!/usr/bin/python3

import pytest
import bpy
from .. import math as g
from ..nodetrees import GeometryNodeTree


test_unary_operations = [
(g.sqrt, "SQRT"),
(g.inverse_sqrt, "INVERSE_SQRT"),
(g.exp, "EXPONENT"),
(g.sign, "SIGN"),
(g.fract, "FRACT"),
(g.sin, "SINE"),
(g.cos, "COSINE"),
(g.tan, "TANGENT"),
(g.asin, "ARCSINE"),
(g.acos, "ARCCOSINE"),
(g.atan, "ARCTANGENT"),
(g.sinh, "SINH"),
(g.cosh, "COSH"),
(g.tanh, "TANH"),
]


test_binary_operations = [
(g.log, "LOGARITHM"),
(g.power, "POWER"),
(g.minimum, "MINIMUM"),
(g.maximum, "MAXIMUM"),
(g.snap, "SNAP"),
(g.pingpong, "PINGPONG"),
(g.atan2, "ARCTAN2"),
]


test_swapped_operations = [
(g.step, "LESS_THAN"), # Swapped meaning: greater than or equal to.
(g.drop, "GREATER_THAN"), # Swapped meaning: smaller than or equal to.
]


test_tertiary_operations = [
(g.multiply_add, "MULTIPLY_ADD"),
(g.compare, "COMPARE"),
(g.smooth_min, "SMOOTH_MIN"),
(g.smooth_max, "SMOOTH_MAX"),
(g.wrap, "WRAP"),
]


@pytest.mark.parametrize("test_function,test_operation_name", test_tertiary_operations)
def test_tertiary(test_function, test_operation_name: str):
tree = GeometryNodeTree("test_add_input")
input1 = tree.InputFloat()
input2 = tree.InputFloat()
input3 = tree.InputFloat()
output = test_function(input1, input2, input3)
# Check if the sockets are linked:
bl_node = output.socket_reference.node
assert isinstance(bl_node, bpy.types.ShaderNodeMath)
assert bl_node.operation == test_operation_name
assert bl_node.inputs[0].is_linked
assert bl_node.inputs[1].is_linked
assert bl_node.inputs[2].is_linked
# Check if the sockets link correctly:
group_inputs = tree.node_tree.nodes["Group Input"].outputs
assert isinstance(bl_node.inputs[0].links, tuple)
assert bl_node.inputs[0].links[0].from_socket == group_inputs[0]
assert isinstance(bl_node.inputs[1].links, tuple)
assert bl_node.inputs[1].links[0].from_socket == group_inputs[1]
assert isinstance(bl_node.inputs[2].links, tuple)
assert bl_node.inputs[2].links[0].from_socket == group_inputs[2]


@pytest.mark.parametrize("test_function,test_operation_name", test_binary_operations)
def test_binary(test_function, test_operation_name: str):
tree = GeometryNodeTree("test_add_input")
input1 = tree.InputFloat()
input2 = tree.InputFloat()
output = test_function(input1, input2)
# Check if the sockets are linked:
bl_node = output.socket_reference.node
assert isinstance(bl_node, bpy.types.ShaderNodeMath)
assert bl_node.operation == test_operation_name
assert bl_node.inputs[0].is_linked
assert bl_node.inputs[1].is_linked
assert not bl_node.inputs[2].is_linked
# Check if the sockets link correctly:
group_inputs = tree.node_tree.nodes["Group Input"].outputs
assert isinstance(bl_node.inputs[0].links, tuple)
assert bl_node.inputs[0].links[0].from_socket == group_inputs[0]
assert isinstance(bl_node.inputs[1].links, tuple)
assert bl_node.inputs[1].links[0].from_socket == group_inputs[1]


@pytest.mark.parametrize("test_function,test_operation_name", test_swapped_operations)
def test_swapped_binary(test_function, test_operation_name: str):
tree = GeometryNodeTree("test_add_input")
input1 = tree.InputFloat()
input2 = tree.InputFloat()
output = test_function(input1, input2)
# Check if the sockets are linked:
bl_node = output.socket_reference.node
assert isinstance(bl_node, bpy.types.ShaderNodeMath)
assert bl_node.operation == test_operation_name
assert bl_node.inputs[0].is_linked
assert bl_node.inputs[1].is_linked
assert not bl_node.inputs[2].is_linked
# Check if the sockets link correctly:
group_inputs = tree.node_tree.nodes["Group Input"].outputs
assert isinstance(bl_node.inputs[0].links, tuple)
assert bl_node.inputs[0].links[0].from_socket == group_inputs[1]
assert isinstance(bl_node.inputs[1].links, tuple)
assert bl_node.inputs[1].links[0].from_socket == group_inputs[0]


@pytest.mark.parametrize("test_function,test_operation_name", test_unary_operations)
def test_unary(test_function, test_operation_name: str):
tree = GeometryNodeTree("test_add_input")
input1 = tree.InputFloat()
output = test_function(input1)
# Check if the sockets are linked:
bl_node = output.socket_reference.node
assert isinstance(bl_node, bpy.types.ShaderNodeMath)
assert bl_node.operation == test_operation_name
assert bl_node.inputs[0].is_linked
assert not bl_node.inputs[1].is_linked
assert not bl_node.inputs[2].is_linked
# Check if the sockets link correctly:
group_inputs = tree.node_tree.nodes["Group Input"].outputs
assert isinstance(bl_node.inputs[0].links, tuple)
assert bl_node.inputs[0].links[0].from_socket == group_inputs[0]
60 changes: 60 additions & 0 deletions modules/geoscript/tests/test_nodetrees.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/python3

import pytest
import bpy
from ..nodetrees import GeometryNodeTree


def test_geometry_node_tree_class() -> None:
"""Tests whether a simple GeometryNodeTree class can correctly add an input."""
class ExampleTree(GeometryNodeTree):
def function(self):
# Add new nodes to the tree:
input1 = self.InputGeometry()

example_tree = ExampleTree("test_function")
bl_tree = example_tree.get_bl_tree()
assert isinstance(bl_tree, bpy.types.GeometryNodeTree)
inputs = bl_tree.inputs
assert len(inputs) == 1
first_input = inputs[0]
assert isinstance(first_input, bpy.types.NodeSocketInterfaceStandard)
assert first_input.type == "GEOMETRY"
assert isinstance(first_input, bpy.types.NodeSocketInterfaceGeometry)


def add_input(example_tree: GeometryNodeTree, bpy_type: str) -> None:
if bpy_type == "VALUE":
example_tree.InputFloat()
elif bpy_type == "BOOLEAN":
example_tree.InputBoolean()
elif bpy_type == "VECTOR":
example_tree.InputVector()
elif bpy_type == "OBJECT":
example_tree.InputObject()
elif bpy_type == "GEOMETRY":
example_tree.InputGeometry()


test_input_types = [
("VALUE", bpy.types.NodeSocketInterfaceFloat),
("BOOLEAN", bpy.types.NodeSocketInterfaceBool),
("VECTOR", bpy.types.NodeSocketInterfaceVector),
("OBJECT", bpy.types.NodeSocketInterfaceObject),
("GEOMETRY", bpy.types.NodeSocketInterfaceGeometry),
]


@pytest.mark.parametrize("test_bpy_typename,test_bpy_type", test_input_types)
def test_add_input(test_bpy_typename: str, test_bpy_type: type) -> None:
"""Tests whether the input generation functions work correctly."""
example_tree = GeometryNodeTree("test_add_input")
add_input(example_tree, test_bpy_typename)
bl_tree = example_tree.get_bl_tree()
assert isinstance(bl_tree, bpy.types.GeometryNodeTree)
inputs = bl_tree.inputs
assert len(inputs) == 1
first_input = inputs[0]
assert isinstance(first_input, bpy.types.NodeSocketInterfaceStandard)
assert first_input.type == test_bpy_typename
assert isinstance(first_input, test_bpy_type)

0 comments on commit f4dd8c5

Please sign in to comment.