Python testing in Rhino

Hello all,

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?

EDIT - I just found the test samples on github using the unittest module

Thanks for any advice.

Graham

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?

Thanks,

Graham

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?

1 Like

Thanks for the reply Tadashi,

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.

Here is a working sample file,

1 Like

Great - that looks good!

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)

@Dancergraham can you give some practical examples of what you want to test?

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

1 Like

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?

1 Like

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()
1 Like

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()

Hi Gijs,

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

-Willem

haha, well, it might even generate something useful, or at least you can get an extra fully objective opinion in the mix :smiley:

yes these kind of things are handy to do automatic, because it’s kind of a waste of time, so we can focus on the more subjective parts

yes, that woud be nice if you have them right at hand…

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.

2 Likes

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.

Hi Gijs.

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)

Good luck!
Groeten
-Willem

1 Like