# -*- coding: utf-8 -*- # *************************************************************************** # * Copyright (c) 2016 sliptonic * # * Copyright (c) 2022 Larry Woestman * # * * # * This program is free software; you can redistribute it and/or modify * # * it under the terms of the GNU Lesser General Public License (LGPL) * # * as published by the Free Software Foundation; either version 2 of * # * the License, or (at your option) any later version. * # * for detail see the LICENCE text file. * # * * # * This program 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 Library General Public License for more details. * # * * # * You should have received a copy of the GNU Library General Public * # * License along with this program; if not, write to the Free Software * # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * # * USA * # * * # *************************************************************************** import difflib import os import unittest import FreeCAD import Path import Path.Post.Command as PathPost from PathScripts import PathPreferences import Path.Post.Utils as PostUtils import Path.Post.Processor as PostProcessor # If KEEP_DEBUG_OUTPUT is False, remove the gcode file after the test succeeds. # If KEEP_DEBUG_OUTPUT is True or the test fails leave the gcode file behind # so it can be looked at easily. KEEP_DEBUG_OUTPUT = False PathPost.LOG_MODULE = Path.Log.thisModule() Path.Log.setLevel(Path.Log.Level.INFO, PathPost.LOG_MODULE) class TestPathPost(unittest.TestCase): """Test some of the output of the postprocessors. So far there are three tests each for the linuxcnc and centroid postprocessors. """ def setUp(self): """Set up the postprocessor tests.""" pass def tearDown(self): """Tear down after the postprocessor tests.""" pass # # You can run just this test using: # ./FreeCAD -c -t PathTests.TestPathPost.TestPathPost.test_postprocessors # def test_postprocessors(self): """Test the postprocessors.""" # # The tests are performed in the order they are listed: # one test performed on all of the postprocessors # then the next test on all of the postprocessors, etc. # You can comment out the tuples for tests that you don't want # to use. # tests_to_perform = ( # (output_file_id, freecad_document, job_name, postprocessor_arguments, # postprocessor_list) # # test with all of the defaults (metric mode, etc.) ("default", "boxtest1", "Job", "--no-show-editor", ()), # test in Imperial mode ("imperial", "boxtest1", "Job", "--no-show-editor --inches", ()), # test in metric, G55, M4, the other way around the part ("other_way", "boxtest1", "Job001", "--no-show-editor", ()), # test in metric, split by fixtures, G54, G55, G56 ("split", "boxtest1", "Job002", "--no-show-editor", ()), # test in metric mode without the header ("no_header", "boxtest1", "Job", "--no-header --no-show-editor", ()), # test translating G81, G82, and G83 to G00 and G01 commands ( "drill_translate", "drill_test1", "Job", "--no-show-editor --translate_drill", ("grbl", "refactored_grbl"), ), ) # # The postprocessors to test. # You can comment out any postprocessors that you don't want # to test. # postprocessors_to_test = ( "centroid", # "fanuc", "grbl", "linuxcnc", "mach3_mach4", "refactored_centroid", # "refactored_fanuc", "refactored_grbl", "refactored_linuxcnc", "refactored_mach3_mach4", "refactored_test", ) # # Enough of the path to where the tests are stored so that # they can be found by the python interpreter. # PATHTESTS_LOCATION = "Mod/Path/PathTests" # # The following code tries to re-use an open FreeCAD document # as much as possible. It compares the current document with # the document for the next test. If the names are different # then the current document is closed and the new document is # opened. The final document is closed at the end of the code. # current_document = "" for ( output_file_id, freecad_document, job_name, postprocessor_arguments, postprocessor_list, ) in tests_to_perform: if current_document != freecad_document: if current_document != "": FreeCAD.closeDocument(current_document) current_document = freecad_document current_document_path = ( FreeCAD.getHomePath() + PATHTESTS_LOCATION + os.path.sep + current_document + ".fcstd" ) FreeCAD.open(current_document_path) job = FreeCAD.ActiveDocument.getObject(job_name) # Create the objects to be written by the postprocessor. postlist = PathPost.buildPostList(job) for postprocessor_id in postprocessors_to_test: if postprocessor_list == () or postprocessor_id in postprocessor_list: print( "\nRunning %s test on %s postprocessor:\n" % (output_file_id, postprocessor_id) ) processor = PostProcessor.load(postprocessor_id) output_file_path = FreeCAD.getHomePath() + PATHTESTS_LOCATION output_file_pattern = "test_%s_%s" % ( postprocessor_id, output_file_id, ) output_file_extension = ".ngc" for idx, section in enumerate(postlist): partname = section[0] sublist = section[1] output_filename = PathPost.processFileNameSubstitutions( job, partname, idx, output_file_path, output_file_pattern, output_file_extension, ) # print("output file: " + output_filename) file_path, extension = os.path.splitext(output_filename) reference_file_name = "%s%s%s" % (file_path, "_ref", extension) # print("reference file: " + reference_file_name) gcode = processor.export( sublist, output_filename, postprocessor_arguments ) if not gcode: print("no gcode") with open(reference_file_name, "r") as fp: reference_gcode = fp.read() if not reference_gcode: print("no reference gcode") # Remove the "Output Time:" line in the header from the # comparison if it is present because it changes with # every test. gcode_lines = [ i for i in gcode.splitlines(True) if "Output Time:" not in i ] reference_gcode_lines = [ i for i in reference_gcode.splitlines(True) if "Output Time:" not in i ] if gcode_lines != reference_gcode_lines: msg = "".join( difflib.ndiff(gcode_lines, reference_gcode_lines) ) self.fail( os.path.basename(output_filename) + " output doesn't match:\n" + msg ) if not KEEP_DEBUG_OUTPUT: os.remove(output_filename) if current_document != "": FreeCAD.closeDocument(current_document) class TestPathPostUtils(unittest.TestCase): def test010(self): """Test the utility functions in the PostUtils.py file.""" commands = [ Path.Command("G1 X-7.5 Y5.0 Z0.0"), Path.Command("G2 I2.5 J0.0 K0.0 X-5.0 Y7.5 Z0.0"), Path.Command("G1 X5.0 Y7.5 Z0.0"), Path.Command("G2 I0.0 J-2.5 K0.0 X7.5 Y5.0 Z0.0"), Path.Command("G1 X7.5 Y-5.0 Z0.0"), Path.Command("G2 I-2.5 J0.0 K0.0 X5.0 Y-7.5 Z0.0"), Path.Command("G1 X-5.0 Y-7.5 Z0.0"), Path.Command("G2 I0.0 J2.5 K0.0 X-7.5 Y-5.0 Z0.0"), Path.Command("G1 X-7.5 Y0.0 Z0.0"), ] testpath = Path.Path(commands) self.assertTrue(len(testpath.Commands) == 9) self.assertTrue( len([c for c in testpath.Commands if c.Name in ["G2", "G3"]]) == 4 ) results = PostUtils.splitArcs(testpath) # self.assertTrue(len(results.Commands) == 117) self.assertTrue( len([c for c in results.Commands if c.Name in ["G2", "G3"]]) == 0 ) def dumpgroup(group): print("====Dump Group======") for i in group: print(i[0]) for j in i[1]: print(f"--->{j.Name}") print("====================") class TestBuildPostList(unittest.TestCase): """ The postlist is the list of postprocessable elements from the job. The list varies depending on -The operations -The tool controllers -The work coordinate systems (WCS) or 'fixtures' -How the job is ordering the output (WCS, tool, operation) -Whether or not the output is being split to multiple files This test case ensures that the correct sequence of postable objects is created. The list will be comprised of a list of tuples. Each tuple consists of (subobject string, [list of objects]) The subobject string can be used in output name generation if splitting output the list of objects is all postable elements to be written to that file """ testfile = FreeCAD.getHomePath() + "Mod/Path/PathTests/test_filenaming.fcstd" doc = FreeCAD.open(testfile) job = doc.getObjectsByLabel("MainJob")[0] def setUp(self): pass def tearDown(self): pass def test000(self): # check that the test file is structured correctly self.assertTrue(len(self.job.Tools.Group) == 2) self.assertTrue(len(self.job.Fixtures) == 2) self.assertTrue(len(self.job.Operations.Group) == 3) self.job.SplitOutput = False self.job.OrderOutputBy = "Operation" def test010(self): postlist = PathPost.buildPostList(self.job) self.assertTrue(type(postlist) is list) firstoutputitem = postlist[0] self.assertTrue(type(firstoutputitem) is tuple) self.assertTrue(type(firstoutputitem[0]) is str) self.assertTrue(type(firstoutputitem[1]) is list) def test020(self): # Without splitting, result should be list of one item self.job.SplitOutput = False self.job.OrderOutputBy = "Operation" postlist = PathPost.buildPostList(self.job) self.assertTrue(len(postlist) == 1) def test030(self): # No splitting should include all ops, tools, and fixtures self.job.SplitOutput = False self.job.OrderOutputBy = "Operation" postlist = PathPost.buildPostList(self.job) firstoutputitem = postlist[0] firstoplist = firstoutputitem[1] self.assertTrue(len(firstoplist) == 14) def test040(self): # Test splitting by tool # ordering by tool with toolnumber for string teststring = "%T.nc" self.job.SplitOutput = True self.job.PostProcessorOutputFile = teststring self.job.OrderOutputBy = "Tool" postlist = PathPost.buildPostList(self.job) firstoutputitem = postlist[0] self.assertTrue(firstoutputitem[0] == str(5)) # check length of output firstoplist = firstoutputitem[1] self.assertTrue(len(firstoplist) == 5) def test050(self): # ordering by tool with tool description for string teststring = "%t.nc" self.job.SplitOutput = True self.job.PostProcessorOutputFile = teststring self.job.OrderOutputBy = "Tool" postlist = PathPost.buildPostList(self.job) firstoutputitem = postlist[0] self.assertTrue(firstoutputitem[0] == "TC__7_16__two_flute") def test060(self): # Ordering by fixture and splitting teststring = "%W.nc" self.job.SplitOutput = True self.job.PostProcessorOutputFile = teststring self.job.OrderOutputBy = "Fixture" postlist = PathPost.buildPostList(self.job) firstoutputitem = postlist[0] firstoplist = firstoutputitem[1] self.assertTrue(len(firstoplist) == 6) self.assertTrue(firstoutputitem[0] == "G54") class TestOutputNameSubstitution(unittest.TestCase): """ String substitution allows the following: %D ... directory of the active document %d ... name of the active document (with extension) %M ... user macro directory %j ... name of the active Job object The Following can be used if output is being split. If Output is not split these will be ignored. %S ... Sequence Number (default) %T ... Tool Number %t ... Tool Controller label %W ... Work Coordinate System %O ... Operation Label self.job.Fixtures = ["G54"] self.job.SplitOutput = False self.job.OrderOutputBy = "Fixture" Assume: active document: self.assertTrue(filename, f"{home}/testdoc.fcstd user macro: ~/.local/share/FreeCAD/Macro Job: MainJob Operations: OutsideProfile DrillAllHoles TC: 7/16" two flute (5) TC: Drill (2) Fixtures: (G54, G55) Strings should be sanitized like this to ensure valid filenames # import re # filename="TC: 7/16" two flute" # >>> re.sub(r"[^\w\d-]","_",filename) # "TC__7_16__two_flute" """ testfile = FreeCAD.getHomePath() + "Mod/Path/PathTests/test_filenaming.fcstd" testfilepath, testfilename = os.path.split(testfile) testfilename, ext = os.path.splitext(testfilename) doc = FreeCAD.open(testfile) job = doc.getObjectsByLabel("MainJob")[0] macro = FreeCAD.getUserMacroDir() def test000(self): # Test basic name generation with empty string FreeCAD.setActiveDocument(self.doc.Label) teststring = "" self.job.PostProcessorOutputFile = teststring PathPreferences.setOutputFileDefaults( teststring, "Append Unique ID on conflict" ) self.job.SplitOutput = False outlist = PathPost.buildPostList(self.job) self.assertTrue(len(outlist) == 1) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) self.assertEqual(filename, f"{self.testfilename}.nc") def test015(self): # Test basic string substitution without splitting teststring = "~/Desktop/%j.nc" self.job.PostProcessorOutputFile = teststring PathPreferences.setOutputFileDefaults( teststring, "Append Unique ID on conflict" ) self.job.SplitOutput = False outlist = PathPost.buildPostList(self.job) self.assertTrue(len(outlist) == 1) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) self.assertEqual( os.path.normpath(filename), os.path.normpath("~/Desktop/MainJob.nc") ) def test010(self): # Substitute current file path teststring = "%D/testfile.nc" self.job.PostProcessorOutputFile = teststring PathPreferences.setOutputFileDefaults( teststring, "Append Unique ID on conflict" ) outlist = PathPost.buildPostList(self.job) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) self.assertEqual( os.path.normpath(filename), os.path.normpath(f"{self.testfilepath}/testfile.nc"), ) def test020(self): teststring = "%d.nc" self.job.PostProcessorOutputFile = teststring PathPreferences.setOutputFileDefaults( teststring, "Append Unique ID on conflict" ) outlist = PathPost.buildPostList(self.job) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) self.assertEqual(filename, f"{self.testfilename}.nc") def test030(self): teststring = "%M/outfile.nc" self.job.PostProcessorOutputFile = teststring PathPreferences.setOutputFileDefaults( teststring, "Append Unique ID on conflict" ) outlist = PathPost.buildPostList(self.job) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) self.assertEqual(filename, f"{self.macro}outfile.nc") def test040(self): # unused substitution strings should be ignored teststring = "%d%T%t%W%O/testdoc.nc" self.job.PostProcessorOutputFile = teststring PathPreferences.setOutputFileDefaults( teststring, "Append Unique ID on conflict" ) outlist = PathPost.buildPostList(self.job) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) self.assertEqual( os.path.normpath(filename), os.path.normpath(f"{self.testfilename}/testdoc.nc"), ) def test050(self): # explicitly using the sequence number should include it where indicated. teststring = "%S-%d.nc" self.job.PostProcessorOutputFile = teststring PathPreferences.setOutputFileDefaults( teststring, "Append Unique ID on conflict" ) outlist = PathPost.buildPostList(self.job) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) self.assertEqual(filename, "0-test_filenaming.nc") def test060(self): # # Split by Tool self.job.SplitOutput = True self.job.OrderOutputBy = "Tool" outlist = PathPost.buildPostList(self.job) # substitute jobname and use default sequence numbers teststring = "%j.nc" self.job.PostProcessorOutputFile = teststring PathPreferences.setOutputFileDefaults( teststring, "Append Unique ID on conflict" ) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) self.assertEqual(filename, "MainJob-0.nc") subpart, objs = outlist[1] filename = PathPost.resolveFileName(self.job, subpart, 1) self.assertEqual(filename, "MainJob-1.nc") # Use Toolnumbers and default sequence numbers teststring = "%T.nc" self.job.PostProcessorOutputFile = teststring PathPreferences.setOutputFileDefaults( teststring, "Append Unique ID on conflict" ) outlist = PathPost.buildPostList(self.job) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) self.assertEqual(filename, "5-0.nc") subpart, objs = outlist[1] filename = PathPost.resolveFileName(self.job, subpart, 1) self.assertEqual(filename, "2-1.nc") # Use Tooldescriptions and default sequence numbers teststring = "%t.nc" self.job.PostProcessorOutputFile = teststring PathPreferences.setOutputFileDefaults( teststring, "Append Unique ID on conflict" ) outlist = PathPost.buildPostList(self.job) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) self.assertEqual(filename, "TC__7_16__two_flute-0.nc") subpart, objs = outlist[1] filename = PathPost.resolveFileName(self.job, subpart, 1) self.assertEqual(filename, "TC__Drill-1.nc") def test070(self): # Split by WCS self.job.SplitOutput = True self.job.OrderOutputBy = "Fixture" outlist = PathPost.buildPostList(self.job) teststring = "%j.nc" self.job.PostProcessorOutputFile = teststring PathPreferences.setOutputFileDefaults( teststring, "Append Unique ID on conflict" ) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) self.assertEqual(filename, "MainJob-0.nc") subpart, objs = outlist[1] filename = PathPost.resolveFileName(self.job, subpart, 1) self.assertEqual(filename, "MainJob-1.nc") teststring = "%W-%j.nc" self.job.PostProcessorOutputFile = teststring PathPreferences.setOutputFileDefaults( teststring, "Append Unique ID on conflict" ) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) self.assertEqual(filename, "G54-MainJob-0.nc") subpart, objs = outlist[1] filename = PathPost.resolveFileName(self.job, subpart, 1) self.assertEqual(filename, "G55-MainJob-1.nc") def test080(self): # Split by Operation self.job.SplitOutput = True self.job.OrderOutputBy = "Operation" outlist = PathPost.buildPostList(self.job) teststring = "%j.nc" self.job.PostProcessorOutputFile = teststring PathPreferences.setOutputFileDefaults( teststring, "Append Unique ID on conflict" ) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) self.assertEqual(filename, "MainJob-0.nc") subpart, objs = outlist[1] filename = PathPost.resolveFileName(self.job, subpart, 1) self.assertEqual(filename, "MainJob-1.nc") teststring = "%O-%j.nc" self.job.PostProcessorOutputFile = teststring PathPreferences.setOutputFileDefaults( teststring, "Append Unique ID on conflict" ) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) self.assertEqual(filename, "OutsideProfile-MainJob-0.nc") subpart, objs = outlist[1] filename = PathPost.resolveFileName(self.job, subpart, 1) self.assertEqual(filename, "DrillAllHoles-MainJob-1.nc")