Has anyone got recommendations for methods / frameworks to use in automated testing of python scripts (Rhino 5)? I have a small, growing, library of python scripts which I and several colleagues use to automate tasks such as naming geometry, rebuilding surfaces, importing, exporting and validating geometry.
I would envisage having a set of rhino files which would be automatically loaded, the scripts run and I would use some assert statements to check that the results were as expected. I see that doctest is included in Ironpython - has anyone tried that? Anybody got pytest to work with Ironpython? Anyone else interested in going on this journey towards test driven development (TDD) with me?
I haven’t seen anyone using doctest on here but I found some discussion of unittest
@filip_rooms@tadashi_K@katsu did you ever manage to get unittest working with Ironpython in Rhino? Do you have any advice to share with a testing noob?
Rhino uses IronPython so to answer your question; yes it works with IronPython. (Check out the following link for more information What version of Python does Rhino use?) Just follow the advice given in the Unittest in Rhino Python and you should be able to get your unittest going. I never tried doctest. What feature of doctest that is missing in unittest?
Good to know you got this working! If I understand correctly then unittest involves creating a python file with all the tests, then importing the module you want to test.
Doctest uses another approach : you put sample code inside the docstrings in the module you want to test and the docstring module checks that they run correctly.
They therefore serve a dual purpose as documentation and tests.
To illustrate the docstring approach UPDATE - modified so it works in Rhino - save this as testy.py :
def vcrit(b, n, st):
"""Calculate critical wind speed for vortex shedding.
args:
b = crosswind dimension in m
n = natural frequency in Hz
st = Strouhal number
returns:
critical wind velocity for resonance due to vortex shedding in m/s.
>>> vcrit(0.2,0.3,0.18)
0.33333333333333331
"""
return b * n / st
if __name__ == '__main__':
from doctest import testmod
from imp import reload
import testy
reload(testy)
testmod(m=testy, verbose = True)
Oh cool! I like how documentation is integrated into each method. I tried in RhinoPython it works fine. You can either use Rhino Python Editor or Run a Python file, both works fine.
I haven’t tried Python in Grasshopper (GhPython). My understanding is it might be hard to perform unittest or doctest in GhPython. Maybe someone who is more knowledgeable can help you on doctest in GhPython.
Strange that you need to import the module itself and specify the name in the testmod function. It seems that Ironpython doesn’t add '__main__' to sys.modules. This is not the case in the CPython 2.7 interpreter. I also added an imp.reload call so I can experiment with changing the docstrings without having to restart the intepreter:
def vcrit(b, n, st):
"""Calculate critical wind speed for vortex shedding.
args:
b = crosswind dimension in m
n = natural frequency in Hz
st = Strouhal number
returns:
critical wind velocity for resonance due to vortex shedding in m/s.
>>> vcrit(0.2,0.3,0.18)
0.33333333333333331
"""
return b * n / st
if __name__ == '__main__':
from doctest import testmod
from imp import reload
import testy
reload(testy)
testmod(m=testy, verbose = True)
Hi @Gijs, Yes here is one that I use regularly with arbitrary complex geometry- I guess the checks would be to make sure the geometry is scaled down and back up properly and that the saved values in the document data are updated properly, including with undo events
Aha…that’s nice. I’ll need some time to think about possible applications, but this is certainly interesting. Any tips btw for a quick tutorial one how to use github?
Not really - if you find a good one then I am interested! I started out by creating an account, then a gist (simple space for sharing a single script) before experimenting with GitHub integration in Pycharm. I still get a bit confused as to whether I have successfully pushed updates to the repo!
I was just brewing some coffee, and thinking about this and I might have a good application for it. 3 times a year I need to check student’s files (about 50 rhino files) on a number of criteria. Would be awesome if I could automate this. One of them for example is checking if all the detail drawings have a correct scale. Wonder if this is possible…
Yes that sounds possible, with some modifications. There are likely to be some manual steps unless you specify thing like a given insertion point or provide a template for them to work in…
well one of the things is that details should have distinct scales (like 1:1, 1:5, 1:10) but not 1:1.324 or 1:3, 1:7)
I just checked and it seems quite easy to check this:
import Rhino
import scriptcontext as sc
def getPageDetails():
# Get all page views
page_views = sc.doc.Views.GetPageViews()
if page_views:
# Process each page view
for page_view in page_views:
print "processing page " + page_view.PageName
details = page_view.GetDetailViews()
if details:
# Process each page view detail
for detail in details:
rc = detail.DetailGeometry.PageToModelRatio
if rc == 0:
print "perspective view"
else:
print "detail scale is 1: " , (1/rc)
getPageDetails()
I think the below should check and report the right back to test if all scales are set correctly, although it then still needs to correspond with the page scale (not sure if this can somehow be a retrievable parameter?)
next then would be: process a directory, process this code and put the result in a nice table…
import Rhino
import scriptcontext as sc
def getPageDetails():
total_pages = 0
total_details = 0
right = 0
wrong = 0
perspective = 0
# Get all page views
page_views = sc.doc.Views.GetPageViews()
if page_views:
# Process each page view
for page_view in page_views:
total_pages +=1
#print "processing page " + page_view.PageName
details = page_view.GetDetailViews()
if details:
# Process each page view detail
for detail in details:
total_details +=1
rc = detail.DetailGeometry.PageToModelRatio
if rc == 0:
#print "perspective view"
perspective +=1
else:
#print "detail scale is 1: " , (1/rc)
if rc == 0.5 or rc==1 or rc == 2:
#print "right scale 0"
right+=1
elif rc>1:
if rc%5 != 0:
wrong+=1
else:
right+=1
else:
if (1/rc)%5 != 0:
wrong+=1
else:
right+=1
if total_pages>0:
print "Document has ",total_pages, " pages and ", total_details, " details."
print "Of all ", total_details," found details ", right, " were right and ",\
wrong, " were wrong. ", perspective, " were perspective views."
else:
print "Lazy student - No layout pages found in this document."
getPageDetails()
That’s smart thinking, automating the check process.
Extrapolating you could create a ‘feedback’ generator providing a choice from standardized lines. Composed based on file content and some input variables.
Kidding aside , automating the feedback process is probably going to save quite some time.
checking for Units
checking for Detail scale
checking amount of details/annotations and raising a flag if it’s only a few
checking for number of layers and flag if only a few or way too many
as for
let me know if you need some help with that I cab provide the snippets to build this
In order to have the complete picture of Python in Rhino here, I will add a note on doctest or unittest with Python in Grasshopper. You can use doctest with Grasshopper Python. You will not be using the native IronPython in Grasshopper; however you can use the GH_CPython by MahmoudAbdelRahman. (https://www.food4rhino.com/app/ghcpython) This CPython node runs doctest without any problem.
Ahh yes. That looks interesting. Unfortunately I am not an administrator on my machine so I cannot install it, and I don’t really use GrassHopper anyway at the moment so it’s not a priority for me.
I composed a setup to process all 3dm files in a folder and subfolders gijstest.zip (16.2 KB)
the script is in the zip as well:
# coding=utf-8
import os
import rhinoscriptsyntax as rs
import scriptcontext as sc
def count_parts():
# method returning a sting for the report
return len(rs.AllObjects())
def process_file(filepath):
"""
open the file and create report string
write report string to txt file with same name as file
"""
#create filepath for report
base_filepath = os.path.splitext(filepath)[0]
report_filepath = base_filepath+'.txt'
#open the file
sc.doc.Modified = False
rs.Command('_-Open "{}" _Enter'.format(filepath))
#collect data
units = sc.doc.ModelUnitSystem
count = count_parts()
#costruct report string
report = ''
report += 'Units : {}'.format(units)
report += '\n'
report += 'object count : {}'.format(count)
#write report to file
with open(report_filepath, 'w','utf-8') as f:
f.write(report)
def report_folders(this_dir):
filepaths = []
#collect all 3dm filepaths in all forlder sand subfolders
for root, dirs, files in os.walk(this_dir):
for file in files:
extension = os.path.splitext(file)[1]
if extension.lower() == '.3dm':
filepath = os.path.join(root,file)
filepaths.append(filepath)
#process all files found
for filepath in filepaths:
process_file(filepath)
#Open new empty file
sc.doc.Modified = False
rs.Command('_-New _Enter')
if __name__ == '__main__':
#this_dir is the directory where the script is located
#you could aso choose for a rs.BrowseForFolder
this_dir = os.path.dirname( __file__ )
report_folders(this_dir)