New demo module to teach developers how to hack on the BlenderBIM Add-on.

This commit is contained in:
Dion Moult
2022-01-26 21:27:41 +11:00
parent 36a7985f5a
commit 1d47ad3cd4
17 changed files with 796 additions and 0 deletions
@@ -68,6 +68,8 @@ modules = {
"covetool": None,
"augin": None,
"debug": None,
# Uncomment this line to enable loading of the demo module. Happy hacking!
# "demo": None,
}
@@ -0,0 +1,55 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
# ############################################################################ #
# Hey there! Welcome to the BlenderBIM Add-on code. Please feel free to reach
# out if you have any questions or need further guidance. Happy hacking!
# ############################################################################ #
# Every module has a __init__.py file to load all of its classes. Every
# operation, property, and interface needs to be registered with the Blender
# system when the add-on loads. This is where it happens.
import bpy
from . import ui, prop, operator
# You'll need to provide a list of every one of your classes here. If you forget
# to specify your class, it won't load and you won't be able to use that
# operator, property, or interface.
classes = (
operator.DemonstrateHelloWorld,
operator.DemonstrateRenameProject,
prop.BIMDemoProperties,
ui.BIM_PT_demo,
)
# When the add-on loads, this register function is called. This allows you to
# perform additional tasks during startup. If you need to store custom
# properties, this is where you tell Blender where they are going to be stored.
# You might see more advanced registrations happening in other modules.
def register():
bpy.types.Scene.BIMDemoProperties = bpy.props.PointerProperty(type=prop.BIMDemoProperties)
# When someone disables the add-on, we need to unload everything we loaded. This
# does the reverse of the register function.
def unregister():
del bpy.types.Scene.BIMDemoProperties
@@ -0,0 +1,78 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
# ############################################################################ #
# Hey there! Welcome to the BlenderBIM Add-on code. Please feel free to reach
# out if you have any questions or need further guidance. Happy hacking!
# ############################################################################ #
# Every module has a data.py file to load data for its interface panels to
# display. When a panel needs to show information that doesn't come from a
# Blender property, it gets that data from one of these classes. This separation
# of "fetching data" and "displaying data" is a common concept that makes UI
# code much simpler, more efficient, and less error-prone to refresh data.
import blenderbim.tool as tool
# All data must have a refresh function. The refresh function simply sets all
# data class to be not yet loaded. So the next time the interface is drawn to
# the user, it will force the data to be reloaded.
def refresh():
# When you define your own data classes, just add to this list!
DemoData.is_loaded = False
# This is a sample data class. It correlates to a single interface panel. Panels
# should not share data classes. This makes it easy to write your interface
# without having your code mixed in with other parts of the interface. As a
# convention, the class is named the same name as the panel.
class DemoData:
# All data classes must have two variables. One to store all the data it has
# loaded and another to store the load state.
data = {}
is_loaded = False
# Every data class must have a load function. This lets us load data in a
# predictable manner.
@classmethod
def load(cls):
# The load function always has two responsibilities: populate the data,
# and set is_loaded to true.
cls.data = {
"has_project": cls.has_project(),
"project_name": cls.project_name(),
}
cls.is_loaded = True
@classmethod
def has_project(cls):
# Here, we check whether or not there is an active IFC project in our
# Blender session.
return bool(tool.Ifc.get())
@classmethod
def project_name(cls):
# Imagine how messy our UI code would be if all of this was mixed in
# with our layout code. Here, it's isolated and testable, and the UX
# becomes easier to maintain and change.
ifc = tool.Ifc.get()
if ifc:
return ifc.by_type("IfcProject")[0].Name or "Unnamed"
@@ -0,0 +1,79 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
# ############################################################################ #
# Hey there! Welcome to the BlenderBIM Add-on code. Please feel free to reach
# out if you have any questions or need further guidance. Happy hacking!
# ############################################################################ #
# Every module has an operator.py file to define all of the buttons that a user
# can click on from the Blender interface. Blender calls these buttons
# "Operators", since they correlate to a single user operation.
import bpy
import blenderbim.tool as tool
import blenderbim.core.demo as core
import blenderbim.bim.handler
# Each button correlates to a class like the one below. In this case, we're
# creating a new button that will execute a hello world feature.
class DemonstrateHelloWorld(bpy.types.Operator, tool.Ifc.Operator):
# Every operator has a unique ID. If you enable Python tooltips and hover
# over any button in the interface, you will see each button will run a
# function that uses this ID. For example, hovering over this button will
# show that the code it executes is "bpy.ops.bim.demonstrate_hello_world()"
bl_idname = "bim.demonstrate_hello_world"
# In the interface, this button will have the text "Print Hello World".
bl_label = "Print Hello World"
# This code means that the user can undo or redo after pressing the button.
bl_options = {"REGISTER", "UNDO"}
# When hovering over the button, this helpful description will be shown.
bl_description = "Prints the text 'Hello World'"
# When the button is pressed, this _execute() function will run.
def _execute(self, context):
# Every operator should do one thing only: execute a core function. In
# order to execute a core function, the operator's responsibility is to
# pass in all of the inputs the core needs to do its job.
# A core function simply tells tools what to do, so a core function will
# always need at least one tool as an input.
core.demonstrate_hello_world(tool.Demo)
# This class, just like the one above, will correlate to another button in the
# interface. It has a different ID and a different label.
class DemonstrateRenameProject(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.demonstrate_rename_project"
bl_label = "Rename IFC Project"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Renames your IFC project name and prints the name"
def _execute(self, context):
# This core function requires three inputs: two tools, and a name
# string. In this case, the name is taken from some custom Blender
# properties. Generally, the inputs to the core function will come from
# properties (such as an input field) or data from the scene (like the
# actively selected object).
core.demonstrate_rename_project(tool.Ifc, tool.Demo, name=bpy.context.scene.BIMDemoProperties.name)
@@ -0,0 +1,37 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from blenderbim.bim.prop import StrProperty, Attribute
from bpy.types import PropertyGroup
from bpy.props import (
PointerProperty,
StringProperty,
EnumProperty,
BoolProperty,
IntProperty,
FloatProperty,
FloatVectorProperty,
CollectionProperty,
)
class BIMDemoProperties(PropertyGroup):
name: StringProperty(name="Name", default="New Project Name")
message: StringProperty(name="Message")
show_hints: BoolProperty(name="Show Hints", default=False)
@@ -0,0 +1,131 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
# ############################################################################ #
# Hey there! Welcome to the BlenderBIM Add-on code. Please feel free to reach
# out if you have any questions or need further guidance. Happy hacking!
# ############################################################################ #
# Every module has a ui.py file to define its interface. Interfaces describe how
# panels, buttons, labels, and input fields are laid out.
import bpy
from blenderbim.bim.module.demo.data import DemoData
# Every panel in the interface correlates to one of these classes. If you enable
# the "Developer Extras" option in Blender, then you can actually right click on
# any panel in Blender, and click "Edit Source". This will bring you to a ui.py
# file like this one where you can see this code. Pretty neat!
class BIM_PT_demo(bpy.types.Panel):
# Every panel has a title.
bl_label = "BlenderBIM Demo"
# Every panel must have an ID. It must be unique. For example, you may want
# to later reference the panel (such as if you want to create a nested
# subpanel).
bl_idname = "BIM_PT_demo"
# This tells the panel to appear in the properties section (in the bottom
# right by default) of the Blender interface.
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
# This tells the panel to appear in the "scene" tab of the properties panel.
bl_context = "scene"
# Every panel has a draw function. This draws all the layout of the panel,
# including all of its labels, buttons, and so on. Note that if the panel is
# hidden, this draw function will not be called.
def draw(self, context):
# Before drawing any content, the panel must load any dynamic variables
# that it will show. Most panels have dynamic content, which display
# changing data from your BIM model. You must load that data first. The
# two lines are always the same, "if not loaded, then load".
if not DemoData.is_loaded:
# Each panel should have its own data class that it loads data from.
# Note that we only load data if it hasn't already been loaded.
# This is because interface panels are drawn continuously. This
# draw() function will be called every time you scroll or move your
# mouse over it. Loading data is slow, so we only refresh data when
# we have to.
DemoData.load()
# Interface panels often show properties. For convenience, define where
# the properties are stored for the module.
self.props = context.scene.BIMDemoProperties
# This defines a new "row" in our layout. When a new row is defined, the
# things on that row, like buttons, labels, and input fields, show on a
# new line.
row = self.layout.row()
# This is the simplest interface element - a label placed in our row.
# If you enable the "Icon Viewer" add-on, you can view a list of
# Blender's built-in icons to choose from in the Blender text editor's
# side panel.
row.label(text="This is a demo panel", icon="INFO")
# Our interface can contain simple logic. For example, if we don't have
# an IFC project, show an error message and don't draw anything else.
# The ui.py should always have very simple logic. Details like how to
# determine whether we have a project is delegated to the data loader.
if not DemoData.data["has_project"]:
row = self.layout.row()
row.label(text="Load or create an IFC project first", icon="ERROR")
return
row = self.layout.row()
# When you want to show a button in the interface, you have to specify
# an operator. Operators are defined in operator.py. You reference an
# operator using its unique ID.
row.operator("bim.demonstrate_hello_world")
# Blender properties can be used to affect what your panel shows. Notice
# how the logic is very simple. Only use simple boolean predicates and
# loops in your layout. No complex logic should ever been seen in the
# interface code.
if self.props.message:
row = self.layout.row()
row.label(text=self.props.message)
row = self.layout.row(align=True)
row.label(text="Project Name")
# Sometimes you need data that doesn't come from Blender properties,
# like from your IFC model. In this case, just get some data from your
# data loader. For those familiar with how templating languages work,
# this is exactly the same.
row.label(text=DemoData.data["project_name"])
row = self.layout.row()
# We've seen how to show text labels and buttons using operators. What
# about text input fields, number sliders, dropdowns, checkboxes, and
# scrollable lists? The way Blender works is that you simply show a
# Blender property in your interface. The data type of that property
# determines which UI widget is shown. A text property will show a text
# field. A number property will show a number slider. An enum property
# will show a drop down. And so on. In this case, our name property is a
# text data type, so expect to see a text input field show up here.
row.prop(self.props, "name")
row = self.layout.row()
# Here's another button, referencing another operator.
row.operator("bim.demonstrate_rename_project")
if self.props.show_hints:
row = self.layout.row()
row.label(text="Name cannot be blank!")
+73
View File
@@ -0,0 +1,73 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
# ############################################################################ #
# Hey there! Welcome to the BlenderBIM Add-on code. Please feel free to reach
# out if you have any questions or need further guidance. Happy hacking!
# ############################################################################ #
# Every module has a core.py file to define all of its core functions. A core
# function describes what happens when the user wants to do something like
# pressing a button.
# Think of a core function as a short poem of pseudocode that describes what
# happens in different usecases. A core should be no more than 50 lines of code,
# even in the most complex of features. A core simply delegates tasks to tools
# in a sequence that describes the flow of logic in a feature - in other words,
# it tells tools to do different things. You will notice that the core doesn't
# have any code that deals with Blender or IFC directly - these are all little
# details that are hidden away in tools. The core is not interested in these
# details, the core is only concerned with the big picture.
# Imagine, no matter how complex a software can be, every feature can be
# described in regular sentences in under 50 lines. That is the purpose of the
# core.
# Here's the simplest possible core function. It does one thing only. Remember:
# core functions delegate tasks to tools, so all core functions need at least
# one tool.
def demonstrate_hello_world(demo):
# We're telling the demo tool to set a message. We aren't interested how the
# tool works, that's a detail. We aren't interested in the interface, like
# where the message is shown. You can name these functions whatever you feel
# best describes what's going on, like if you had to describe the feature to
# someone else.
demo.set_message("Hello, World!")
# Here's a slightly more complex core function. It uses two tools. By default,
# you'll want to use your module's tool (in this case, "Demo") for all tasks,
# except for changing IFC data, where you'll use the "Ifc" tool. At a glance,
# simply by seeing what tools are used, this gives you an idea about what
# aspects (or dependencies) a core function cares about. This function also has
# an non-tool input called "name". Non-tool inputs should be keyword arguments
# with possible default values.
def demonstrate_rename_project(ifc, demo, name=None):
# As you can see, core functions read almost like pseudocode. A great way to
# code a new feature is to write out what it does in English first, then
# change them into tool functions. You can choose any function you want, and
# you can see a list of every single tool function in # `core/tool.py`.
if name:
project = demo.get_project()
ifc.run("attribute.edit_attributes", product=project, attributes={"Name": name})
demo.clear_name_field()
demo.hide_user_hints()
else:
demo.show_user_hints()
+32
View File
@@ -30,6 +30,38 @@ def interface(cls):
return type(cls.__name__, (Interface, cls), attrs)
# ############################################################################ #
# Hey there! Welcome to the BlenderBIM Add-on code. Please feel free to reach
# out if you have any questions or need further guidance. Happy hacking!
# ############################################################################ #
# This portion of the code is singled out as it is part of the Demo module, used
# to teach people how the code is structured in the BlenderBIM Add-on.
# This is an example "interface" (as in, a software interface, not a user
# interface) for the Demo module. Think of it as a list of all of the functions
# your module can perform. In this file, you will come across every single
# capability of the entire add-on. By breaking it down into little functions, we
# can test each one separately, and at a glance know if there are opportunities
# for code reuse or refactoring. At this point, we're only interested in a list
# of functions, not the details of how they're implemented. This is because the
# purpose of the core is to just give a high level overview of the application's
# capabilities.
@interface
class Demo:
def clear_name_field(cls): pass
def get_project(cls): pass
def hide_user_hints(cls): pass
def set_message(cls, message): pass
def show_user_hints(cls): pass
# The rest of the code in this file is not part of the Demo tutorial.
# ############################################################################ #
@interface
class Aggregate:
def can_aggregate(cls, relating_object, related_object): pass
@@ -23,6 +23,7 @@ from blenderbim.tool.brick import Brick
from blenderbim.tool.collector import Collector
from blenderbim.tool.context import Context
from blenderbim.tool.debug import Debug
from blenderbim.tool.demo import Demo
from blenderbim.tool.drawing import Drawing
from blenderbim.tool.geometry import Geometry
from blenderbim.tool.ifc import Ifc
+66
View File
@@ -0,0 +1,66 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
# ############################################################################ #
# Hey there! Welcome to the BlenderBIM Add-on code. Please feel free to reach
# out if you have any questions or need further guidance. Happy hacking!
# ############################################################################ #
# Every module has a tool file which implements all the functions that the core
# needs. Whereas the core is simply high level code, the tool file has the
# concrete implementations, dealing with exactly how things interact with
# Blender's property systems, IFC's data structures, the filesystem, geometry
# processing, and more.
import bpy
import blenderbim.core.tool
import blenderbim.tool as tool
# There is always one class in each tool file, which implements the interface
# defined by `core/tool.py`.
class Demo(blenderbim.core.tool.Demo):
@classmethod
def clear_name_field(cls):
# In this concrete implementation, we see that "clear name field"
# actually translates to "set this Blender string property to empty
# string". In this case, it's pretty simple - but even simple scenarios
# like these are important to implement in the tool, as it makes the
# pseudocode easier to read in the core, and makes it easier to test
# implementations separately from control flow. It also makes it easy to
# refactor and share functions, where every tool function is captured by
# a function name that describes its intention.
bpy.context.scene.BIMDemoProperties.name = ""
@classmethod
def get_project(cls):
return tool.Ifc.get().by_type("IfcProject")[0]
@classmethod
def hide_user_hints(cls):
bpy.context.scene.BIMDemoProperties.show_hints = False
@classmethod
def set_message(cls, message):
bpy.context.scene.BIMDemoProperties.message = message
@classmethod
def show_user_hints(cls):
bpy.context.scene.BIMDemoProperties.show_hints = True
+6
View File
@@ -65,3 +65,9 @@ class Ifc(blenderbim.core.tool.Ifc):
@classmethod
def unlink(cls, element=None, obj=None):
IfcStore.unlink_element(element, obj)
class Operator:
def execute(self, context):
IfcStore.execute_ifc_operator(self, context)
blenderbim.bim.handler.refresh_ui_data()
return {"FINISHED"}
+1
View File
@@ -6,6 +6,7 @@ markers =
brick
context
debug
demo
drawing
geometry
library
@@ -0,0 +1,45 @@
# ############################################################################ #
# Hey there! Welcome to the BlenderBIM Add-on code. Please feel free to reach
# out if you have any questions or need further guidance. Happy hacking!
# ############################################################################ #
# This allows us to write full integration tests that test how all systems work
# as a whole. Whilst other tests focus on portions of the software, these tests
# simulate what happens when a user opens Blender, presses buttons, and does
# things.
# These tests read like english. You can see all the possible sentences defined
# in test_feature.py. Most of the time, there is already a sentence defined for
# what you want to test.
@demo
Feature: Demo
# Every operator has at least one scenario associated with it to test it.
Scenario: Demonstrate hello world
Given an empty IFC project
When I press "bim.demonstrate_hello_world"
# Blender doesn't have a way of testing that things are visible in the
# interface and layout. We can check properties, and whats in the 3D
# scenegraph, but not layout. There is no "DOM" like in web applications.
# Too bad, we can't check the results, but we still write the test, that way
# we can still check for errors like crashes or Python errors, like a "smoke
# test".
Then nothing happens
# This operator has two scenarios because there are two possibilities of a user
# interacting with it.
Scenario: Demonstrate rename project - with a name provided
Given an empty IFC project
When I set "scene.BIMDemoProperties.name" to "Foobar"
And I press "bim.demonstrate_rename_project"
Then the object "IfcProject/Foobar" is an "IfcProject"
# This is the other possible scenario for the rename project operator.
Scenario: Demonstrate rename project - with no name
Given an empty IFC project
When I set "scene.BIMDemoProperties.name" to ""
And I press "bim.demonstrate_rename_project"
Then the object "IfcProject/My Project" is an "IfcProject"
+13
View File
@@ -209,6 +209,19 @@ def i_set_prop_to_value(prop, value):
exec(f"bpy.context.{prop} = {value}")
@given(parsers.parse('I set "{prop}" to ""'))
@when(parsers.parse('I set "{prop}" to ""'))
def i_set_prop_to_value(prop):
try:
eval(f"bpy.context.{prop}")
except:
assert False, "Property does not exist"
try:
exec(f'bpy.context.{prop} = r""')
except:
pass
@when(parsers.parse('I am on frame "{number}"'))
def i_am_on_frame_number(number):
bpy.context.scene.frame_set(int(number))
+7
View File
@@ -70,6 +70,13 @@ def debug():
prophet.verify()
@pytest.fixture
def demo():
prophet = Prophecy(blenderbim.core.tool.Demo)
yield prophet
prophet.verify()
@pytest.fixture
def drawing():
prophet = Prophecy(blenderbim.core.tool.Drawing)
+83
View File
@@ -0,0 +1,83 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
# ############################################################################ #
# Hey there! Welcome to the BlenderBIM Add-on code. Please feel free to reach
# out if you have any questions or need further guidance. Happy hacking!
# ############################################################################ #
# Testing the core might seem strange if you haven't written this type of
# abstract test before. You essentially want to test that things are called in
# the right sequence. This seems almost like writing the code twice, like in
# double entry bookkeeping in accounting. However, it guards against typos,
# helps document intention of different logical flows, and ensures that all
# permutations of logic flows meet sanity checks.
# We always call the module we're testing the "test subject". This makes our
# tests simple to read: just look for where the "subject" is called!
import blenderbim.core.demo as subject
# These are like mocks, stubs, or spy objects. They don't do anything, but they
# let us check our test expectations.
from test.core.bootstrap import ifc, demo
# Let's test the hello world function.
class TestDemonstrateHelloWorld:
# This function has only one logical flow, there is no benefit to describing
# it further, so we have a test_run function. We need to specify all the
# mock objects we need in the signature.
def test_run(self, demo):
# We set an expectation that in this default sequence of events, the
# demo tool should have the set_message called with the "Hello, World!"
# string as its argument. Notice how our test also reads like English.
demo.set_message("Hello, World!").should_be_called()
# After we've finished specifying our test expectations, let's run the
# test subject!
subject.demonstrate_hello_world(demo)
# Another test, but this time with two logical flows.
class TestDemonstrateRenameProject:
# The default logical flow is where we rename the project successfully.
def test_renaming_the_project(self, ifc, demo):
# This time, we describe an expectation that the demo tool should have
# its get_project() function called with no attributes. When it is
# called, we expect it to return "project" as a string.
# This might sound strange. How is get_project implemented? Does it
# actually return a string? We don't know, and we don't care. That's a
# detail that our core isn't interested in. All our core is interested
# in is that we get back a project - we're arbitrarily using a string to
# represent it.
demo.get_project().should_be_called().will_return("project")
# Here, again, we're not interested in the details. However, we are
# interested in checking that the project we previously retrieved is
# passed verbatim into the Ifc tool.
ifc.run("attribute.edit_attributes", product="project", attributes={"Name": "name"}).should_be_called()
demo.clear_name_field().should_be_called()
demo.hide_user_hints().should_be_called()
# Let's test our test subject! Notice that just like the arbitrary
# string "project", we've specified an arbitrary input string of "name".
subject.demonstrate_rename_project(ifc, demo, name="name")
# One alternative flow is when no name is provided. What happens then?
def test_showing_a_hint_if_no_name_provided(self, ifc, demo):
demo.show_user_hints().should_be_called()
subject.demonstrate_rename_project(ifc, demo, name=None)
+87
View File
@@ -0,0 +1,87 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
# ############################################################################ #
# Hey there! Welcome to the BlenderBIM Add-on code. Please feel free to reach
# out if you have any questions or need further guidance. Happy hacking!
# ############################################################################ #
# Because our tools have well defined, isolated functions, it means we can test
# them very easily in isolation. Tests are fun, fast, and easy to setup!
import bpy
import ifcopenshell
import blenderbim.core.tool
import blenderbim.tool as tool
from test.bim.bootstrap import NewFile
from blenderbim.tool.demo import Demo as subject
# Our first test is that our tool implements all the abstract methods defined by
# the `core/tool.py` interface. Anytime the core wants something that the tools
# don't provide, this test will catch it. This type of test comes for free in
# other languages, but not Python, so we test it explicitly.
class TestImplementsTool(NewFile):
def test_run(self):
assert isinstance(subject(), blenderbim.core.tool.Demo)
# These are fairly boring tests. What it does demonstrate is that no matter how
# complex a BIM application can get, it can always be reduced down to small,
# easily tested functions. Note that these functions actually are concrete
# implementations, so that means that you need to use Blender headlessly to run
# these tests.
class TestClearNameField(NewFile):
def test_run(self):
bpy.context.scene.BIMDemoProperties.name = "name"
subject.clear_name_field()
assert bpy.context.scene.BIMDemoProperties.name == ""
class TestGetProject(NewFile):
def test_run(self):
# Sometimes, there is a bit of preparation work to setup a scenario that
# can be tested. In this case, we need to set an active IFC dataset with
# a project. This is normal.
ifc = ifcopenshell.file()
project = ifc.createIfcProject()
tool.Ifc.set(ifc)
assert subject.get_project() == project
class TestHideUserHints(NewFile):
def test_run(self):
bpy.context.scene.BIMDemoProperties.show_hints = True
subject.hide_user_hints()
assert bpy.context.scene.BIMDemoProperties.show_hints == False
class TestSetMessage(NewFile):
def test_run(self):
bpy.context.scene.BIMDemoProperties.message = ""
subject.set_message("message")
assert bpy.context.scene.BIMDemoProperties.message == "message"
class TestShowUserHints(NewFile):
def test_run(self):
bpy.context.scene.BIMDemoProperties.show_hints = False
subject.show_user_hints()
assert bpy.context.scene.BIMDemoProperties.show_hints == True