diff --git a/src/Mod/CAM/CMakeLists.txt b/src/Mod/CAM/CMakeLists.txt index bb763cfcd9..1dacd878c8 100644 --- a/src/Mod/CAM/CMakeLists.txt +++ b/src/Mod/CAM/CMakeLists.txt @@ -147,6 +147,7 @@ SET(PathPythonPostScripts_SRCS Path/Post/scripts/fanuc_post.py Path/Post/scripts/fangling_post.py Path/Post/scripts/gcode_pre.py + Path/Post/scripts/generic_post.py Path/Post/scripts/grbl_post.py Path/Post/scripts/heidenhain_post.py Path/Post/scripts/jtech_post.py diff --git a/src/Mod/CAM/Path/Main/Job.py b/src/Mod/CAM/Path/Main/Job.py index ec8d633367..830cfabe6c 100644 --- a/src/Mod/CAM/Path/Main/Job.py +++ b/src/Mod/CAM/Path/Main/Job.py @@ -20,7 +20,7 @@ # * * # *************************************************************************** -from Path.Post.Processor import PostProcessor +from Path.Post.Processor import PostProcessorFactory # PostProcessor, from PySide import QtCore from PySide.QtCore import QT_TRANSLATE_NOOP import FreeCAD @@ -112,7 +112,9 @@ class ObjectJob: "App::PropertyFile", "PostProcessorOutputFile", "Output", - QT_TRANSLATE_NOOP("App::Property", "The G-code output file for this project"), + QT_TRANSLATE_NOOP( + "App::Property", "The G-code output file for this project" + ), ) obj.addProperty( "App::PropertyEnumeration", @@ -549,7 +551,7 @@ class ObjectJob: def onChanged(self, obj, prop): if prop == "PostProcessor" and obj.PostProcessor: - processor = PostProcessor.load(obj.PostProcessor) + processor = PostProcessorFactory.get_post_processor(obj, obj.PostProcessor) self.tooltip = processor.tooltip self.tooltipArgs = processor.tooltipArgs diff --git a/src/Mod/CAM/Path/Post/Command.py b/src/Mod/CAM/Path/Post/Command.py index 042fcd4689..7cd45f54f2 100644 --- a/src/Mod/CAM/Path/Post/Command.py +++ b/src/Mod/CAM/Path/Post/Command.py @@ -27,21 +27,17 @@ Processor entries in PathJob """ import FreeCAD import FreeCADGui import Path -import Path.Base.Util as PathUtil -import Path.Main.Job as PathJob from PathScripts import PathUtils +from Path.Post.Utils import FilenameGenerator import os -import re - -from Path.Post.Processor import PostProcessor +from Path.Post.Processor import PostProcessor, PostProcessorFactory from PySide import QtCore, QtGui -from datetime import datetime from PySide.QtCore import QT_TRANSLATE_NOOP LOG_MODULE = Path.Log.thisModule() -debugmodule = False -if debugmodule: +DEBUG = False +if DEBUG: Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) Path.Log.trackModule(Path.Log.thisModule()) else: @@ -51,372 +47,29 @@ else: translate = FreeCAD.Qt.translate -class _TempObject: - Path = None - Name = "Fixture" - InList = [] - Label = "Fixture" - - -def processFileNameSubstitutions( - job, - subpartname, - sequencenumber, - outputpath, - filename, - ext, -): - """Process any substitutions in the outputpath or filename.""" - - # The following section allows substitution within the path part - Path.Log.track(f"path before substitution: {outputpath}") - - if "%D" in outputpath: # Directory of active document - D = FreeCAD.ActiveDocument.FileName - if D: - D = os.path.dirname(D) - # in case the document is in the current working directory - if not D: - D = "." - else: - FreeCAD.Console.PrintError( - "Please save document in order to resolve output path!\n" - ) - return None - outputpath = outputpath.replace("%D", D) - - if "%M" in outputpath: - M = FreeCAD.getUserMacroDir() - outputpath = outputpath.replace("%M", M) - - # Use the file label - if "%d" in outputpath: - d = FreeCAD.ActiveDocument.Label - outputpath = outputpath.replace("%d", d) - - # Use the name of the active job object - if "%j" in outputpath: - j = job.Label - outputpath = outputpath.replace("%j", j) - - Path.Log.track(f"path after substitution: {outputpath}") - - # The following section allows substitution within the filename part - Path.Log.track(f"filename before substitution: {filename}") - - # Use the file label - if "%d" in filename: - d = FreeCAD.ActiveDocument.Label - filename = filename.replace("%d", d) - - # Use the name of the active job object - if "%j" in filename: - j = job.Label - filename = filename.replace("%j", j) - - # Use the sequence number if explicitly called - if "%S" in filename: - j = job.Label - filename = filename.replace("%S", str(sequencenumber)) - - # This section handles unique names for splitting output - if job.SplitOutput: - Path.Log.track() - if "%T" in filename and job.OrderOutputBy == "Tool": - filename = filename.replace("%T", subpartname) - - if "%t" in filename and job.OrderOutputBy == "Tool": - filename = filename.replace("%t", subpartname) - - if "%W" in filename and job.OrderOutputBy == "Fixture": - filename = filename.replace("%W", subpartname) - - if "%O" in filename and job.OrderOutputBy == "Operation": - filename = filename.replace("%O", subpartname) - - if ( - "%S" in filename - ): # We always add a sequence number but the user can say where - filename = filename.replace("%S", str(sequencenumber)) - else: - filename = f"{filename}-{sequencenumber}" - - Path.Log.track(f"filename after substitution: {filename}") - - if not ext: - ext = ".nc" - Path.Log.track(f"file extension: {ext}") - - fullPath = f"{outputpath}{os.path.sep}{filename}{ext}" - - Path.Log.track(f"full filepath: {fullPath}") - return fullPath - - -def resolveFileName(job, subpartname, sequencenumber): - """Generate the file name to use as output.""" - - Path.Log.track(subpartname, sequencenumber) - - validPathSubstitutions = ["D", "d", "M", "j"] - validFilenameSubstitutions = ["j", "d", "T", "t", "W", "O", "S"] - - # Look for preference default - outputpath, filename = os.path.split(Path.Preferences.defaultOutputFile()) - filename, ext = os.path.splitext(filename) - - # Override with document default if it exists - if job.PostProcessorOutputFile: - candidateOutputPath, candidateFilename = os.path.split( - job.PostProcessorOutputFile - ) - - if candidateOutputPath: - outputpath = candidateOutputPath - - if candidateFilename: - filename, ext = os.path.splitext(candidateFilename) - - # Strip any invalid substitutions from the outputpath - for match in re.findall("%(.)", outputpath): - if match not in validPathSubstitutions: - outputpath = outputpath.replace(f"%{match}", "") - - # if nothing else, use current directory - if not outputpath: - outputpath = "." - - # Strip any invalid substitutions from the filename - for match in re.findall("%(.)", filename): - if match not in validFilenameSubstitutions: - filename = filename.replace(f"%{match}", "") - - # if no filename, use the active document label - if not filename: - filename = FreeCAD.ActiveDocument.Label - - # if no extension, use something sensible - if not ext: - ext = ".nc" - - # By now we should have a sanitized path, filename and extension to work with - Path.Log.track(f"path: {outputpath} name: {filename} ext: {ext}") - - fullPath = processFileNameSubstitutions( - job, - subpartname, - sequencenumber, - outputpath, - filename, - ext, - ) - - # This section determines whether user interaction is necessary - policy = Path.Preferences.defaultOutputPolicy() - - openDialog = policy == "Open File Dialog" - # if os.path.isdir(filename) or not os.path.isdir(os.path.dirname(filename)): - # # Either the entire filename resolves into a directory or the parent - # # directory doesn't exist. - # # Either way I don't know what to do - ask for help - # openDialog = True - - if not FreeCAD.GuiUp: # if testing, or scripting, never open dialog. - policy = "Append Unique ID on conflict" - openDialog = False - - if os.path.isfile(fullPath) and not openDialog: - if policy == "Open File Dialog on conflict": - openDialog = True - elif policy == "Append Unique ID on conflict": - fn, ext = os.path.splitext(fullPath) - nr = fn[-3:] - n = 1 - if nr.isdigit(): - n = int(nr) - while os.path.isfile(f"{fn}{n:03d}{ext}"): - n = n + 1 - fullPath = f"{fn}{n:03d}{ext}" - - if openDialog: - requestedfile = QtGui.QFileDialog.getSaveFileName( - QtGui.QApplication.activeWindow(), "Output File", fullPath - ) - if requestedfile[0]: - fullPath = requestedfile[0] - else: - fullPath = None - - if fullPath: - # remove any unused substitution strings: - for s in validPathSubstitutions + validFilenameSubstitutions: - fullPath = fullPath.replace(f"%{s}", "") - fullPath = os.path.normpath(fullPath) - Path.Log.track(fullPath) - - return fullPath - - -def fixtureSetup(order, fixture, job): - """Convert a Fixure setting to _TempObject instance with a G0 move to a - safe height every time the fixture coordinate system change. Skip - the move for first fixture, to avoid moving before tool and tool - height compensation is enabled. - - """ - - fobj = _TempObject() - c1 = Path.Command(fixture) - fobj.Path = Path.Path([c1]) - # Avoid any tool move after G49 in preamble and before tool change - # and G43 in case tool height compensation is in use, to avoid - # dangerous move without tool compesation. - if order != 0: - c2 = Path.Command( - "G0 Z" - + str( - job.Stock.Shape.BoundBox.ZMax - + job.SetupSheet.ClearanceHeightOffset.Value - ) - ) - fobj.Path.addCommands(c2) - fobj.InList.append(job) - return fobj - - -def buildPostList(job): - """Takes the job and determines the specific objects and order to - postprocess Returns a list of objects which can be passed to - exportObjectsWith() for final posting.""" - wcslist = job.Fixtures - orderby = job.OrderOutputBy - - postlist = [] - - if orderby == "Fixture": - Path.Log.debug("Ordering by Fixture") - # Order by fixture means all operations and tool changes will be - # completed in one fixture before moving to the next. - - currTool = None - for index, f in enumerate(wcslist): - # create an object to serve as the fixture path - sublist = [fixtureSetup(index, f, job)] - - # Now generate the gcode - for obj in job.Operations.Group: - tc = PathUtil.toolControllerForOp(obj) - if tc is not None and PathUtil.opProperty(obj, "Active"): - if tc.ToolNumber != currTool: - sublist.append(tc) - Path.Log.debug(f"Appending TC: {tc.Name}") - currTool = tc.ToolNumber - sublist.append(obj) - postlist.append((f, sublist)) - - elif orderby == "Tool": - Path.Log.debug("Ordering by Tool") - # Order by tool means tool changes are minimized. - # all operations with the current tool are processed in the current - # fixture before moving to the next fixture. - - toolstring = "None" - currTool = None - - # Build the fixture list - fixturelist = [] - for index, f in enumerate(wcslist): - # create an object to serve as the fixture path - fixturelist.append(fixtureSetup(index, f, job)) - - # Now generate the gcode - curlist = [] # list of ops for tool, will repeat for each fixture - sublist = [] # list of ops for output splitting - - Path.Log.track(job.PostProcessorOutputFile) - for idx, obj in enumerate(job.Operations.Group): - Path.Log.track(obj.Label) - - # check if the operation is active - if not getattr(obj, "Active", True): - Path.Log.track() - continue - - # Determine the proper string for the Op's TC - tc = PathUtil.toolControllerForOp(obj) - if tc is None: - tcstring = "None" - elif "%T" in job.PostProcessorOutputFile: - tcstring = f"{tc.ToolNumber}" - else: - tcstring = re.sub(r"[^\w\d-]", "_", tc.Label) - Path.Log.track(toolstring) - - if tc is None or tc.ToolNumber == currTool: - curlist.append(obj) - elif tc.ToolNumber != currTool and currTool is None: # first TC - sublist.append(tc) - curlist.append(obj) - currTool = tc.ToolNumber - toolstring = tcstring - - elif tc.ToolNumber != currTool and currTool is not None: # TC - for fixture in fixturelist: - sublist.append(fixture) - sublist.extend(curlist) - postlist.append((toolstring, sublist)) - sublist = [tc] - curlist = [obj] - currTool = tc.ToolNumber - toolstring = tcstring - - if idx == len(job.Operations.Group) - 1: # Last operation. - for fixture in fixturelist: - sublist.append(fixture) - sublist.extend(curlist) - - postlist.append((toolstring, sublist)) - - elif orderby == "Operation": - Path.Log.debug("Ordering by Operation") - # Order by operation means ops are done in each fixture in - # sequence. - currTool = None - - # Now generate the gcode - for obj in job.Operations.Group: - - # check if the operation is active - if not getattr(obj, "Active", True): - continue - - sublist = [] - Path.Log.debug(f"obj: {obj.Name}") - - for index, f in enumerate(wcslist): - sublist.append(fixtureSetup(index, f, job)) - tc = PathUtil.toolControllerForOp(obj) - if tc is not None: - if job.SplitOutput or (tc.ToolNumber != currTool): - sublist.append(tc) - currTool = tc.ToolNumber - sublist.append(obj) - postlist.append((obj.Label, sublist)) - - if job.SplitOutput: - Path.Log.track() - return postlist - - Path.Log.track() - finalpostlist = [ - ("allitems", [item for slist in postlist for item in slist[1]]) - ] - return finalpostlist +def _resolve_post_processor_name(job): + Path.Log.debug("_resolve_post_processor_name()") + if job.PostProcessor: + valid_name = job.PostProcessor + elif Path.Preferences.defaultPostProcessor(): + valid_name = Path.Preferences.defaultPostProcessor() + elif FreeCAD.GuiUp: + valid_name = ( + DlgSelectPostProcessor().exec_() + ) # Ensure DlgSelectPostProcessor is defined + else: + valid_name = None + + if valid_name and PostProcessor.exists(valid_name): + return valid_name + else: + raise ValueError(f"Post processor not identified.") class DlgSelectPostProcessor: """Provide user with list of available and active post processor choices.""" + def __init__(self): self.dialog = FreeCADGui.PySideUic.loadUi(":/panels/DlgSelectPostProcessor.ui") firstItem = None @@ -454,18 +107,6 @@ class DlgSelectPostProcessor: class CommandPathPost: - subpart = 1 - - def resolvePostProcessor(self, job): - if hasattr(job, "PostProcessor"): - post = Path.Preferences.defaultPostProcessor() - if job.PostProcessor: - post = job.PostProcessor - if post and PostProcessor.exists(post): - return post - dlg = DlgSelectPostProcessor() - return dlg.exec_() - def GetResources(self): return { "Pixmap": "CAM_Post", @@ -475,144 +116,97 @@ class CommandPathPost: } def IsActive(self): - if FreeCAD.ActiveDocument is not None: - if FreeCADGui.Selection.getCompleteSelection(): - for o in FreeCAD.ActiveDocument.Objects: - if o.Name[:3] == "Job": - return True + selected = FreeCADGui.Selection.getSelectionEx() + if len(selected) != 1: + return False - return False + selected_object = selected[0].Object + self.candidate = PathUtils.findParentJob(selected_object) - def exportObjectsWith(self, objs, partname, job, sequence, extraargs=None): - Path.Log.track(extraargs) - # check if the user has a project and has set the default post and - # output filename - # extraargs can be passed in at this time - Path.Log.track(partname, sequence) - Path.Log.track(objs) + return self.candidate is not None - # partname = objs[0] - # slist = objs[1] - Path.Log.track(objs, partname) + def _write_file(self, filename, gcode, policy): + if policy == "Open File Dialog": + dlg = QtGui.QFileDialog() + dlg.setFileMode(QtGui.QFileDialog.FileMode.AnyFile) + dlg.setAcceptMode(QtGui.QFileDialog.AcceptMode.AcceptSave) + dlg.setDirectory(os.path.dirname(filename)) + dlg.selectFile(os.path.basename(filename)) + if dlg.exec_(): + filename = dlg.selectedFiles()[0] + Path.Log.debug(filename) + with open(filename, "w") as f: + f.write(gcode) - postArgs = Path.Preferences.defaultPostProcessorArgs() - if hasattr(job, "PostProcessorArgs") and job.PostProcessorArgs: - postArgs = job.PostProcessorArgs - elif hasattr(job, "PostProcessor") and job.PostProcessor: - postArgs = "" + elif policy == "Append Unique ID on conflict": + while os.path.isfile(filename): + base, ext = os.path.splitext(filename) + filename = f"{base}-1{ext}" + with open(filename, "w") as f: + f.write(gcode) - if extraargs is not None: - postArgs += f" {extraargs}" - - Path.Log.track(postArgs) - - postname = self.resolvePostProcessor(job) - # filename = "-" - filename = resolveFileName(job, partname, sequence) - # if postname and needFilename: - # filename = resolveFileName(job) - - if postname and filename: - print("post: %s(%s, %s)" % (postname, filename, postArgs)) - processor = PostProcessor.load(postname) - gcode = processor.export(objs, filename, postArgs) - return (False, gcode, filename) - else: - return (True, "", filename) + elif policy == "Open File Dialog on conflict": + if os.path.isfile(filename): + dlg = QtGui.QFileDialog() + dlg.setFileMode(QtGui.QFileDialog.FileMode.AnyFile) + dlg.setAcceptMode(QtGui.QFileDialog.AcceptSave) + dlg.setDirectory(os.path.dirname(filename)) + dlg.selectFile(os.path.basename(filename)) + if dlg.exec_(): + filename = dlg.selectedFiles()[0] + Path.Log.debug(filename) + with open(filename, "w") as f: + f.write(gcode) + else: + with open(filename, "w") as f: + f.write(gcode) + else: # Overwrite + with open(filename, "w") as f: + f.write(gcode) + FreeCAD.Console.PrintMessage(f"File written to {filename}\n") def Activated(self): - Path.Log.track() - FreeCAD.ActiveDocument.openTransaction("Post Process the Selected path(s)") - FreeCADGui.addModule("Path.Post.Command") + """ + Handles the activation of post processing, initiating the process based + on user selection and document context. + """ + Path.Log.debug(self.candidate.Name) + FreeCAD.ActiveDocument.openTransaction("Post Process the Selected Job") - # Attempt to figure out what the user wants to post-process - # If a job is selected, post that. - # If there's only one job in a document, post it. - # If a user has selected a subobject of a job, post the job. - # If multiple jobs and can't guess, ask them. + postprocessor_name = _resolve_post_processor_name(self.candidate) + Path.Log.debug(f"Post Processor: {postprocessor_name}") - selected = FreeCADGui.Selection.getSelectionEx() - if len(selected) > 1: - FreeCAD.Console.PrintError( - "Please select a single job or other path object\n" - ) - return - elif len(selected) == 1: - sel = selected[0].Object - if sel.Name[:3] == "Job": - job = sel - elif hasattr(sel, "Path"): - try: - job = PathUtils.findParentJob(sel) - except Exception: - job = None - else: - job = None - - if job is None: - targetlist = [] - for o in FreeCAD.ActiveDocument.Objects: - if hasattr(o, "Proxy"): - if isinstance(o.Proxy, PathJob.ObjectJob): - targetlist.append(o.Label) - Path.Log.debug(f"Possible post objects: {targetlist}") - if len(targetlist) > 1: - jobname, result = QtGui.QInputDialog.getItem( - None, translate("Path", "Choose a Path Job"), None, targetlist - ) - - if result is False: - return - else: - jobname = targetlist[0] - job = FreeCAD.ActiveDocument.getObject(jobname) - - Path.Log.debug(f"about to postprocess job: {job.Name}") - - postlist = buildPostList(job) - # filename = resolveFileName(job, "allitems", 0) - - filenames = [] - - success = True - finalgcode = "" - for idx, section in enumerate(postlist): - partname = section[0] - sublist = section[1] - - result, gcode, name = self.exportObjectsWith(sublist, partname, job, idx) - filenames.append(name) - Path.Log.track(result, gcode, name) - - if name is None: - success = False - else: - finalgcode += gcode - - # if job.SplitOutput: - # for idx, sublist in enumerate(postlist): # name, slist in postlist: - # result = self.exportObjectsWith(sublist[1], sublist[0], job, idx) - - # if result is None: - # success = False - # else: - # gcode += result - - # else: - # finalpostlist = [item for (_, slist) in postlist for item in slist] - # gcode = self.exportObjectsWith(finalpostlist, "allitems", job, 1) - # success = gcode is not None - - Path.Log.track(success) - if success: - if hasattr(job, "LastPostProcessDate"): - job.LastPostProcessDate = str(datetime.now()) - if hasattr(job, "LastPostProcessOutput"): - job.LastPostProcessOutput = " \n".join(filenames) - Path.Log.track(job.LastPostProcessOutput) - FreeCAD.ActiveDocument.commitTransaction() - else: + if not postprocessor_name: FreeCAD.ActiveDocument.abortTransaction() + return + + # get a postprocessor + postprocessor = PostProcessorFactory.get_post_processor( + self.candidate, postprocessor_name + ) + + post_data = postprocessor.export() + if not post_data: + FreeCAD.ActiveDocument.abortTransaction() + return + + policy = Path.Preferences.defaultOutputPolicy() + generator = FilenameGenerator(job=self.candidate) + generated_filename = generator.generate_filenames() + + for item in post_data: + subpart, gcode = item + + # get a name for the file + subpart = "" if subpart == "allitems" else subpart + Path.Log.debug(subpart) + generator.set_subpartname(subpart) + fname = next(generated_filename) + + # write the results to the file + self._write_file(fname, gcode, policy) + + FreeCAD.ActiveDocument.commitTransaction() FreeCAD.ActiveDocument.recompute() diff --git a/src/Mod/CAM/Path/Post/Processor.py b/src/Mod/CAM/Path/Post/Processor.py index 64573a0dcc..ee68ab48ea 100644 --- a/src/Mod/CAM/Path/Post/Processor.py +++ b/src/Mod/CAM/Path/Post/Processor.py @@ -19,62 +19,327 @@ # * USA * # * * # *************************************************************************** - -import Path -import sys +""" +The base classes for post processors in CAM workbench. +""" +from PySide import QtCore, QtGui from importlib import reload +import FreeCAD +import Path +import Path.Base.Util as PathUtil +import importlib.util +import os +import sys +import re Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) +debug = False +if debug: + Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) + Path.Log.trackModule(Path.Log.thisModule()) +else: + Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) + + +class _TempObject: + Path = None + Name = "Fixture" + InList = [] + Label = "Fixture" + + +class PostProcessorFactory: + """Factory class for creating post processors.""" + + @staticmethod + def get_post_processor(job, postname): + # Log initial debug message + Path.Log.debug("PostProcessorFactory.get_post_processor()") + + # Posts have to be in a place we can find them + syspath = sys.path + paths = Path.Preferences.searchPathsPost() + paths.extend(sys.path) + + module_name = f"{postname}_post" + class_name = postname.title() + + # Iterate all the paths to find the module + for path in paths: + module_path = os.path.join(path, f"{module_name}.py") + spec = importlib.util.spec_from_file_location(module_name, module_path) + + if spec and spec.loader: + module = importlib.util.module_from_spec(spec) + try: + spec.loader.exec_module(module) + Path.Log.debug(f"found module {module_name} at {module_path}") + + except (FileNotFoundError, ImportError, ModuleNotFoundError): + continue + + try: + PostClass = getattr(module, class_name) + return PostClass(job) + except AttributeError: + # Return an instance of WrapperPost if no valid module is found + Path.Log.debug(f"Post processor {postname} is a script") + return WrapperPost(job, module_path) + class PostProcessor: + """Base Class. All postprocessors should inherit from this class.""" + + def __init__(self, job, tooltip, tooltipargs, units, *args, **kwargs): + self._tooltip = tooltip + self._tooltipargs = tooltipargs + self._units = units + self._job = job + self._args = args + self._kwargs = kwargs + @classmethod def exists(cls, processor): return processor in Path.Preferences.allAvailablePostProcessors() - @classmethod - def load(cls, processor): - Path.Log.track(processor) - syspath = sys.path - paths = Path.Preferences.searchPathsPost() - paths.extend(sys.path) - sys.path = paths + def export(self): + raise NotImplementedError("Subclass must implement abstract method") - postname = processor + "_post" - namespace = {} + @property + def tooltip(self): + """Get the tooltip text for the post processor.""" + raise NotImplementedError("Subclass must implement abstract method") + # return self._tooltip - # can't modify function local scope with exec in python3 - exec("import %s as current_post" % postname, namespace) - current_post = namespace["current_post"] + @property + def tooltipArgs(self): + """Get the tooltip arguments for the post processor.""" + raise NotImplementedError("Subclass must implement abstract method") + # return self._tooltipargs - # make sure the script is reloaded if it was previously loaded - # should the script have been imported for the first time above - # then the initialization code of the script gets executed twice - # resulting in 2 load messages if the script outputs one of those. + @property + def units(self): + """Get the units used by the post processor.""" + return self._units - exec("reload(%s)" % "current_post") + def _buildPostList(self): + """ + determines the specific objects and order to + postprocess Returns a list of objects which can be passed to + exportObjectsWith() for final posting.""" - sys.path = syspath + def __fixtureSetup(order, fixture, job): + """Convert a Fixure setting to _TempObject instance with a G0 move to a + safe height every time the fixture coordinate system change. Skip + the move for first fixture, to avoid moving before tool and tool + height compensation is enabled. - instance = PostProcessor(current_post) - if hasattr(current_post, "UNITS"): - if current_post.UNITS == "G21": - instance.units = "Metric" - else: - instance.units = "Inch" + """ - if hasattr(current_post, "TOOLTIP"): - instance.tooltip = current_post.TOOLTIP - if hasattr(current_post, "TOOLTIP_ARGS"): - instance.tooltipArgs = current_post.TOOLTIP_ARGS - return instance + fobj = _TempObject() + c1 = Path.Command(fixture) + fobj.Path = Path.Path([c1]) + # Avoid any tool move after G49 in preamble and before tool change + # and G43 in case tool height compensation is in use, to avoid + # dangerous move without toolgg compesation. + if order != 0: + c2 = Path.Command( + "G0 Z" + + str( + job.Stock.Shape.BoundBox.ZMax + + job.SetupSheet.ClearanceHeightOffset.Value + ) + ) + fobj.Path.addCommands(c2) + fobj.InList.append(job) + return fobj - def __init__(self, script): - self.script = script - self.tooltip = None - self.tooltipArgs = None - self.units = None - self.machineName = None + wcslist = self._job.Fixtures + orderby = self._job.OrderOutputBy + Path.Log.debug(f"Ordering by {orderby}") - def export(self, obj, filename, args): - return self.script.export(obj, filename, args) + postlist = [] + + if orderby == "Fixture": + Path.Log.debug("Ordering by Fixture") + # Order by fixture means all operations and tool changes will be + # completed in one fixture before moving to the next. + + currTool = None + for index, f in enumerate(wcslist): + # create an object to serve as the fixture path + sublist = [__fixtureSetup(index, f, self._job)] + + # Now generate the gcode + for obj in self._job.Operations.Group: + tc = PathUtil.toolControllerForOp(obj) + if tc is not None and PathUtil.opProperty(obj, "Active"): + if tc.ToolNumber != currTool: + sublist.append(tc) + Path.Log.debug(f"Appending TC: {tc.Name}") + currTool = tc.ToolNumber + sublist.append(obj) + postlist.append((f, sublist)) + + elif orderby == "Tool": + Path.Log.debug("Ordering by Tool") + # Order by tool means tool changes are minimized. + # all operations with the current tool are processed in the current + # fixture before moving to the next fixture. + + toolstring = "None" + currTool = None + + # Build the fixture list + fixturelist = [] + for index, f in enumerate(wcslist): + # create an object to serve as the fixture path + fixturelist.append(__fixtureSetup(index, f, self._job)) + + # Now generate the gcode + curlist = [] # list of ops for tool, will repeat for each fixture + sublist = [] # list of ops for output splitting + + Path.Log.track(self._job.PostProcessorOutputFile) + for idx, obj in enumerate(self._job.Operations.Group): + Path.Log.track(obj.Label) + + # check if the operation is active + if not getattr(obj, "Active", True): + Path.Log.track() + continue + + # Determine the proper string for the Op's TC + tc = PathUtil.toolControllerForOp(obj) + if tc is None: + tcstring = "None" + elif "%T" in self._job.PostProcessorOutputFile: + tcstring = f"{tc.ToolNumber}" + else: + tcstring = re.sub(r"[^\w\d-]", "_", tc.Label) + Path.Log.track(toolstring) + + if tc is None or tc.ToolNumber == currTool: + curlist.append(obj) + elif tc.ToolNumber != currTool and currTool is None: # first TC + sublist.append(tc) + curlist.append(obj) + currTool = tc.ToolNumber + toolstring = tcstring + + elif tc.ToolNumber != currTool and currTool is not None: # TC + for fixture in fixturelist: + sublist.append(fixture) + sublist.extend(curlist) + postlist.append((toolstring, sublist)) + sublist = [tc] + curlist = [obj] + currTool = tc.ToolNumber + toolstring = tcstring + + if idx == len(self._job.Operations.Group) - 1: # Last operation. + for fixture in fixturelist: + sublist.append(fixture) + sublist.extend(curlist) + + postlist.append((toolstring, sublist)) + + elif orderby == "Operation": + Path.Log.debug("Ordering by Operation") + # Order by operation means ops are done in each fixture in + # sequence. + currTool = None + + # Now generate the gcode + for obj in self._job.Operations.Group: + + # check if the operation is active + if not getattr(obj, "Active", True): + continue + + sublist = [] + Path.Log.debug(f"obj: {obj.Name}") + + for index, f in enumerate(wcslist): + sublist.append(__fixtureSetup(index, f, self._job)) + tc = PathUtil.toolControllerForOp(obj) + if tc is not None: + if self._job.SplitOutput or (tc.ToolNumber != currTool): + sublist.append(tc) + currTool = tc.ToolNumber + sublist.append(obj) + postlist.append((obj.Label, sublist)) + + Path.Log.debug(f"Postlist: {postlist}") + + if self._job.SplitOutput: + Path.Log.track() + return postlist + + Path.Log.track() + finalpostlist = [ + ("allitems", [item for slist in postlist for item in slist[1]]) + ] + Path.Log.debug(f"Postlist: {postlist}") + return finalpostlist + + +class WrapperPost(PostProcessor): + """Wrapper class for old post processors that are scripts.""" + + def __init__(self, job, script_path, *args, **kwargs): + super().__init__( + job, tooltip=None, tooltipargs=None, units=None, *args, **kwargs + ) + self.script_path = script_path + Path.Log.debug(f"WrapperPost.__init__({script_path})") + self.load_script() + + def load_script(self): + # Dynamically load the script as a module + try: + spec = importlib.util.spec_from_file_location( + "script_module", self.script_path + ) + self.script_module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(self.script_module) + except Exception as e: + raise ImportError(f"Failed to load script: {e}") + + if not hasattr(self.script_module, "export"): + raise AttributeError("The script does not have an 'export' function.") + + # Set properties based on attributes of the module + self._units = ( + "Metric" if getattr(self.script_module, "UNITS", "G21") == "G21" else "Inch" + ) + self._tooltip = getattr(self.script_module, "TOOLTIP", "No tooltip provided") + self._tooltipargs = getattr(self.script_module, "TOOLTIP_ARGS", []) + + def export(self): + # Dynamically reload the module for the export to ensure up-to-date usage + + postables = self._buildPostList() + Path.Log.debug(f"postables count: {len(postables)}") + + g_code_sections = [] + for idx, section in enumerate(postables): + partname, sublist = section + + gcode = self.script_module.export(sublist, "-", self._job.PostProcessorArgs) + Path.Log.debug(f"Exported {partname}") + g_code_sections.append((partname, gcode)) + return g_code_sections + + @property + def tooltip(self): + return self._tooltip + + @property + def tooltipArgs(self): + return self._tooltipargs + + @property + def units(self): + return self._units diff --git a/src/Mod/CAM/Path/Post/Utils.py b/src/Mod/CAM/Path/Post/Utils.py index e61ef4db47..1c27e45747 100644 --- a/src/Mod/CAM/Path/Post/Utils.py +++ b/src/Mod/CAM/Path/Post/Utils.py @@ -27,21 +27,154 @@ These are common functions and classes for creating custom post processors. """ -from PySide import QtCore, QtGui - -import FreeCAD - -import Path -import Part from Path.Base.MachineState import MachineState +from PySide import QtCore, QtGui +import FreeCAD +import Part +import Path +import os +import re + +debug = False +if debug: + Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) + Path.Log.trackModule(Path.Log.thisModule()) +else: + Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) translate = FreeCAD.Qt.translate + FreeCADGui = None if FreeCAD.GuiUp: import FreeCADGui +class FilenameGenerator: + def __init__(self, job): + self.job = job + self.subpartname = "" + self.sequencenumber = 0 + path, filename, ext = self.get_path_and_filename_default() + + self.qualified_path = self._apply_path_substitutions(path) + self.qualified_filename = self._apply_filename_substitutions(filename) + self.extension = ext + + def get_path_and_filename_default(self): + outputpath = "" + filename = "" + ext = ".nc" + + validPathSubstitutions = ["D", "d", "M", "j"] + validFilenameSubstitutions = ["j", "d", "T", "t", "W", "O", "S"] + + if self.job.PostProcessorOutputFile: + candidateOutputPath, candidateFilename = os.path.split( + self.job.PostProcessorOutputFile + ) + + if candidateOutputPath: + outputpath = candidateOutputPath + + if candidateFilename: + filename, ext = os.path.splitext(candidateFilename) + else: + outputpath, filename = os.path.split(Path.Preferences.defaultOutputFile()) + filename, ext = os.path.splitext(filename) + + # Make sure we have something to work with + if not filename: + filename = FreeCAD.ActiveDocument.Label + + if not outputpath: + outputpath = os.getcwd() + + if not ext: + ext = ".nc" + + # Check for invalid matches + for match in re.findall("%(.)", outputpath): + Path.Log.debug(f"match: {match}") + if match not in validPathSubstitutions: + outputpath = outputpath.replace(f"%{match}", "") + FreeCAD.Console.PrintWarning( + "Invalid substitution strings will be ignored in output path: %s\n" + % match + ) + + for match in re.findall("%(.)", filename): + Path.Log.debug(f"match: {match}") + if match not in validFilenameSubstitutions: + filename = filename.replace(f"%{match}", "") + FreeCAD.Console.PrintWarning( + "Invalid substitution strings will be ignored in file path: %s\n" + % match + ) + + Path.Log.debug(f"outputpath: {outputpath} filename: {filename} ext: {ext}") + return outputpath, filename, ext + + def set_subpartname(self, subpartname): + self.subpartname = subpartname + + def _apply_path_substitutions(self, file_path): + """Apply substitutions based on job settings and other parameters.""" + substitutions = { + "%D": os.path.dirname(self.job.Document.FileName or "."), + "%d": self.job.Document.Label, + "%j": self.job.Label, + "%M": os.path.dirname(FreeCAD.getUserMacroDir()), + } + for key, value in substitutions.items(): + file_path = file_path.replace(key, value) + + Path.Log.debug(f"file_path: {file_path}") + return file_path + + def _apply_filename_substitutions(self, file_name): + Path.Log.debug(f"file_name: {file_name}") + """Apply substitutions based on job settings and other parameters.""" + substitutions = { + "%d": self.job.Document.Label, + "%j": self.job.Label, + "%T": self.subpartname, # Tool + "%t": self.subpartname, # Tool + "%W": self.subpartname, # Fixture + "%O": self.subpartname, # Operation + } + for key, value in substitutions.items(): + file_name = file_name.replace(key, value) + + Path.Log.debug(f"file_name: {file_name}") + return file_name + + def generate_filenames(self): + """Yield filenames indefinitely with proper substitutions.""" + while True: + temp_filename = self.qualified_filename + Path.Log.debug(f"temp_filename: {temp_filename}") + explicit_sequence = False + matches = re.findall("%S", temp_filename) + if matches: + Path.Log.debug(f"matches: {matches}") + temp_filename = re.sub("%S", str(self.sequencenumber), temp_filename) + explicit_sequence = True + + subpart = f"-{self.subpartname}" if self.subpartname else "" + sequence = ( + f"-{self.sequencenumber}" + if not explicit_sequence and self.sequencenumber + else "" + ) + filename = f"{temp_filename}{subpart}{sequence}{self.extension}" + full_path = os.path.join(self.qualified_path, filename) + + self.sequencenumber += 1 + Path.Log.debug(f"yielding filename: {full_path}") + yield os.path.normpath(full_path) + + class GCodeHighlighter(QtGui.QSyntaxHighlighter): def __init__(self, parent=None): super(GCodeHighlighter, self).__init__(parent) diff --git a/src/Mod/CAM/Path/Post/scripts/dumper_post.py b/src/Mod/CAM/Path/Post/scripts/dumper_post.py index b3d471ae25..4f4209ce31 100644 --- a/src/Mod/CAM/Path/Post/scripts/dumper_post.py +++ b/src/Mod/CAM/Path/Post/scripts/dumper_post.py @@ -27,7 +27,7 @@ import PathScripts.PathUtils as PathUtils from builtins import open as pyopen TOOLTIP = """ -Dumper is an extremely simple postprocessor file for the Path workbench. It is used +Dumper is an extremely simple postprocessor file for the CAM workbench. It is used to dump the command list from one or more Path objects for simple inspection. This post doesn't do any manipulation of the path and doesn't write anything to disk. It just shows the dialog so you can see it. Useful for debugging, but not much else. diff --git a/src/Mod/CAM/Path/Post/scripts/generic_post.py b/src/Mod/CAM/Path/Post/scripts/generic_post.py new file mode 100644 index 0000000000..8b4ec2482a --- /dev/null +++ b/src/Mod/CAM/Path/Post/scripts/generic_post.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2024 Ondsel * +# * * +# * 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 os +from Path.Post.Processor import PostProcessor +import Path +import FreeCAD + +Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) + +translate = FreeCAD.Qt.translate + +debug = True +if debug: + Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) + Path.Log.trackModule(Path.Log.thisModule()) +else: + Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) + + +class Generic(PostProcessor): + def __init__(self, job): + super().__init__( + job, + tooltip=translate("CAM", "Generic post processor"), + tooltipargs=["arg1", "arg2"], + units="kg", + ) + Path.Log.debug("Generic post processor initialized") + + def export(self): + Path.Log.debug("Exporting the job") + + postables = self._buildPostList() + Path.Log.debug(f"postables count: {len(postables)}") + + g_code_sections = [] + for idx, section in enumerate(postables): + partname, sublist = section + + # here is where the sections are converted to gcode. + g_code_sections.append((idx, partname)) + + return g_code_sections + + @property + def tooltip(self): + + tooltip = """ + This is a generic post processor. + It doesn't do anything yet because we haven't immplemented it. + + Implementing it would be a good idea + """ + return tooltip + + @property + def tooltipArgs(self): + argtooltip = """ + --arg1: This is the first argument + --arg2: This is the second argument + + """ + return argtooltip + + @property + def units(self): + return self._units diff --git a/src/Mod/CAM/PathScripts/PathUtils.py b/src/Mod/CAM/PathScripts/PathUtils.py index a584382027..9d02eacce5 100644 --- a/src/Mod/CAM/PathScripts/PathUtils.py +++ b/src/Mod/CAM/PathScripts/PathUtils.py @@ -401,6 +401,9 @@ def findToolController(obj, proxy, name=None): def findParentJob(obj): """retrieves a parent job object for an operation or other Path object""" Path.Log.track() + if hasattr(obj, "Proxy") and isinstance(obj.Proxy, PathJob.ObjectJob): + return obj + for i in obj.InList: if hasattr(i, "Proxy") and isinstance(i.Proxy, PathJob.ObjectJob): return i diff --git a/src/Mod/CAM/TestCAMApp.py b/src/Mod/CAM/TestCAMApp.py index 491ca80769..cd72d79484 100644 --- a/src/Mod/CAM/TestCAMApp.py +++ b/src/Mod/CAM/TestCAMApp.py @@ -44,10 +44,13 @@ from Tests.TestPathHelixGenerator import TestPathHelixGenerator from Tests.TestPathLog import TestPathLog from Tests.TestPathOpUtil import TestPathOpUtil -# from Tests.TestPathPost import TestPathPost +#from Tests.TestPathPost import TestPathPost from Tests.TestPathPost import TestPathPostUtils from Tests.TestPathPost import TestBuildPostList -from Tests.TestPathPost import TestOutputNameSubstitution +# from Tests.TestPathPost import TestOutputNameSubstitution +from Tests.TestPathPost import TestPostProcessorFactory +from Tests.TestPathPost import TestResolvingPostProcessorName +from Tests.TestPathPost import TestFileNameGenerator from Tests.TestPathPreferences import TestPathPreferences from Tests.TestPathProfile import TestPathProfile @@ -83,10 +86,11 @@ False if TestApp.__name__ else True False if TestBuildPostList.__name__ else True False if TestDressupDogbone.__name__ else True False if TestDressupDogboneII.__name__ else True +False if TestFileNameGenerator.__name__ else True False if TestGeneratorDogboneII.__name__ else True False if TestHoldingTags.__name__ else True False if TestPathLanguage.__name__ else True -False if TestOutputNameSubstitution.__name__ else True +# False if TestOutputNameSubstitution.__name__ else True False if TestPathAdaptive.__name__ else True False if TestPathCore.__name__ else True False if TestPathOpDeburr.__name__ else True @@ -96,7 +100,9 @@ False if TestPathHelpers.__name__ else True # False if TestPathHelix.__name__ else True False if TestPathLog.__name__ else True False if TestPathOpUtil.__name__ else True -# False if TestPathPost.__name__ else True +#False if TestPathPost.__name__ else True +False if TestPostProcessorFactory.__name__ else True +False if TestResolvingPostProcessorName.__name__ else True False if TestPathPostUtils.__name__ else True False if TestPathPreferences.__name__ else True False if TestPathProfile.__name__ else True diff --git a/src/Mod/CAM/Tests/TestPathOpUtil.py b/src/Mod/CAM/Tests/TestPathOpUtil.py index fb2326ebee..72b8973cff 100644 --- a/src/Mod/CAM/Tests/TestPathOpUtil.py +++ b/src/Mod/CAM/Tests/TestPathOpUtil.py @@ -34,6 +34,7 @@ Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) DOC = FreeCAD.getHomePath() + "Mod/CAM/Tests/test_geomop.fcstd" + def getWire(obj, nr=0): return obj.Tip.Profile[0].Shape.Wires[nr] @@ -81,6 +82,13 @@ def wireMarkers(wire): class TestPathOpUtil(PathTestUtils.PathTestBase): + @classmethod + def setUpClass(cls): + cls.doc = FreeCAD.openDocument(DOC) + + @classmethod + def tearDownClass(cls): + FreeCAD.closeDocument(cls.doc.Name) def test00(self): """Verify isWireClockwise for polygon wires.""" @@ -137,8 +145,7 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): def test11(self): """Check offsetting a circular hole.""" - doc = FreeCAD.openDocument(DOC) - obj = doc.getObjectsByLabel("offset-circle")[0] + obj = self.doc.getObjectsByLabel("offset-circle")[0] small = getWireInside(obj) self.assertRoughly(10, small.Edges[0].Curve.Radius) @@ -154,12 +161,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): self.assertEqual(1, len(wire.Edges)) self.assertRoughly(0.1, wire.Edges[0].Curve.Radius) self.assertCoincide(Vector(0, 0, 1), wire.Edges[0].Curve.Axis) - FreeCAD.closeDocument("test_geomop") def test12(self): """Check offsetting a circular hole by the radius or more makes the hole vanish.""" - doc = FreeCAD.openDocument(DOC) - obj = doc.getObjectsByLabel("offset-circle")[0] + obj = self.doc.getObjectsByLabel("offset-circle")[0] small = getWireInside(obj) self.assertRoughly(10, small.Edges[0].Curve.Radius) @@ -168,12 +173,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): wire = PathOpUtil.offsetWire(small, obj.Shape, 15, True) self.assertIsNone(wire) - FreeCAD.closeDocument("test_geomop") def test13(self): """Check offsetting a cylinder succeeds.""" - doc = FreeCAD.openDocument(DOC) - obj = doc.getObjectsByLabel("offset-circle")[0] + obj = self.doc.getObjectsByLabel("offset-circle")[0] big = getWireOutside(obj) self.assertRoughly(20, big.Edges[0].Curve.Radius) @@ -189,12 +192,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): self.assertEqual(1, len(wire.Edges)) self.assertRoughly(40, wire.Edges[0].Curve.Radius) self.assertCoincide(Vector(0, 0, -1), wire.Edges[0].Curve.Axis) - FreeCAD.closeDocument("test_geomop") def test14(self): """Check offsetting a hole with Placement.""" - doc = FreeCAD.openDocument(DOC) - obj = doc.getObjectsByLabel("offset-placement")[0] + obj = self.doc.getObjectsByLabel("offset-placement")[0] wires = [ w @@ -215,12 +216,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): self.assertRoughly(8, wire.Edges[0].Curve.Radius) self.assertCoincide(Vector(0, 0, 0), wire.Edges[0].Curve.Center) self.assertCoincide(Vector(0, 0, 1), wire.Edges[0].Curve.Axis) - FreeCAD.closeDocument("test_geomop") def test15(self): """Check offsetting a cylinder with Placement.""" - doc = FreeCAD.openDocument(DOC) - obj = doc.getObjectsByLabel("offset-placement")[0] + obj = self.doc.getObjectsByLabel("offset-placement")[0] wires = [ w @@ -241,12 +240,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): self.assertRoughly(22, wire.Edges[0].Curve.Radius) self.assertCoincide(Vector(0, 0, 0), wire.Edges[0].Curve.Center) self.assertCoincide(Vector(0, 0, -1), wire.Edges[0].Curve.Axis) - FreeCAD.closeDocument("test_geomop") def test20(self): """Check offsetting hole wire succeeds.""" - doc = FreeCAD.openDocument(DOC) - obj = doc.getObjectsByLabel("offset-edge")[0] + obj = self.doc.getObjectsByLabel("offset-edge")[0] small = getWireInside(obj) # sanity check @@ -276,12 +273,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): False, [Vector(0, 4, 0), Vector(-x, -2, 0), Vector(x, -2, 0), Vector(0, 4, 0)], ) - FreeCAD.closeDocument("test_geomop") def test21(self): """Check offsetting hole wire for more than it's size makes hole vanish.""" - doc = FreeCAD.openDocument(DOC) - obj = doc.getObjectsByLabel("offset-edge")[0] + obj = self.doc.getObjectsByLabel("offset-edge")[0] small = getWireInside(obj) # sanity check @@ -299,12 +294,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): ) wire = PathOpUtil.offsetWire(small, obj.Shape, 5, True) self.assertIsNone(wire) - FreeCAD.closeDocument("test_geomop") def test22(self): """Check offsetting a body wire succeeds.""" - doc = FreeCAD.openDocument(DOC) - obj = doc.getObjectsByLabel("offset-edge")[0] + obj = self.doc.getObjectsByLabel("offset-edge")[0] big = getWireOutside(obj) # sanity check @@ -351,12 +344,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): self.assertIsNone("%s: angle=%s" % (type(e.Curve), angle)) lastAngle = angle self.assertTrue(PathOpUtil.isWireClockwise(wire)) - FreeCAD.closeDocument("test_geomop") def test31(self): """Check offsetting a cylinder.""" - doc = FreeCAD.openDocument(DOC) - obj = doc.getObjectsByLabel("circle-cut")[0] + obj = self.doc.getObjectsByLabel("circle-cut")[0] wire = PathOpUtil.offsetWire(getWire(obj.Tool), getPositiveShape(obj), 3, True) self.assertEqual(1, len(wire.Edges)) @@ -372,12 +363,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): self.assertCoincide(Vector(), edge.Curve.Center) self.assertCoincide(Vector(0, 0, +1), edge.Curve.Axis) self.assertRoughly(33, edge.Curve.Radius) - FreeCAD.closeDocument("test_geomop") def test32(self): """Check offsetting a box.""" - doc = FreeCAD.openDocument(DOC) - obj = doc.getObjectsByLabel("square-cut")[0] + obj = self.doc.getObjectsByLabel("square-cut")[0] wire = PathOpUtil.offsetWire(getWire(obj.Tool), getPositiveShape(obj), 3, True) self.assertEqual(8, len(wire.Edges)) @@ -413,12 +402,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): self.assertRoughly(3, e.Curve.Radius) self.assertCoincide(Vector(0, 0, +1), e.Curve.Axis) self.assertFalse(PathOpUtil.isWireClockwise(wire)) - FreeCAD.closeDocument("test_geomop") def test33(self): """Check offsetting a triangle.""" - doc = FreeCAD.openDocument(DOC) - obj = doc.getObjectsByLabel("triangle-cut")[0] + obj = self.doc.getObjectsByLabel("triangle-cut")[0] wire = PathOpUtil.offsetWire(getWire(obj.Tool), getPositiveShape(obj), 3, True) self.assertEqual(6, len(wire.Edges)) @@ -447,12 +434,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): if Part.Circle == type(e.Curve): self.assertRoughly(3, e.Curve.Radius) self.assertCoincide(Vector(0, 0, +1), e.Curve.Axis) - FreeCAD.closeDocument("test_geomop") def test34(self): """Check offsetting a shape.""" - doc = FreeCAD.openDocument(DOC) - obj = doc.getObjectsByLabel("shape-cut")[0] + obj = self.doc.getObjectsByLabel("shape-cut")[0] wire = PathOpUtil.offsetWire(getWire(obj.Tool), getPositiveShape(obj), 3, True) self.assertEqual(6, len(wire.Edges)) @@ -482,12 +467,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): if Part.Circle == type(e.Curve): self.assertRoughly(radius, e.Curve.Radius) self.assertCoincide(Vector(0, 0, +1), e.Curve.Axis) - FreeCAD.closeDocument("test_geomop") def test35(self): """Check offsetting a cylindrical hole.""" - doc = FreeCAD.openDocument(DOC) - obj = doc.getObjectsByLabel("circle-cut")[0] + obj = self.doc.getObjectsByLabel("circle-cut")[0] wire = PathOpUtil.offsetWire(getWire(obj.Tool), getNegativeShape(obj), 3, True) self.assertEqual(1, len(wire.Edges)) @@ -503,12 +486,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): self.assertCoincide(Vector(), edge.Curve.Center) self.assertCoincide(Vector(0, 0, -1), edge.Curve.Axis) self.assertRoughly(27, edge.Curve.Radius) - FreeCAD.closeDocument("test_geomop") def test36(self): """Check offsetting a square hole.""" - doc = FreeCAD.openDocument(DOC) - obj = doc.getObjectsByLabel("square-cut")[0] + obj = self.doc.getObjectsByLabel("square-cut")[0] wire = PathOpUtil.offsetWire(getWire(obj.Tool), getNegativeShape(obj), 3, True) self.assertEqual(4, len(wire.Edges)) @@ -530,12 +511,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): if Path.Geom.isRoughly(e.Vertexes[0].Point.y, e.Vertexes[1].Point.y): self.assertRoughly(54, e.Length) self.assertTrue(PathOpUtil.isWireClockwise(wire)) - FreeCAD.closeDocument("test_geomop") def test37(self): """Check offsetting a triangular holee.""" - doc = FreeCAD.openDocument(DOC) - obj = doc.getObjectsByLabel("triangle-cut")[0] + obj = self.doc.getObjectsByLabel("triangle-cut")[0] wire = PathOpUtil.offsetWire(getWire(obj.Tool), getNegativeShape(obj), 3, True) self.assertEqual(3, len(wire.Edges)) @@ -552,12 +531,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): for e in wire.Edges: self.assertRoughly(length, e.Length) self.assertTrue(PathOpUtil.isWireClockwise(wire)) - FreeCAD.closeDocument("test_geomop") def test38(self): """Check offsetting a shape hole.""" - doc = FreeCAD.openDocument(DOC) - obj = doc.getObjectsByLabel("shape-cut")[0] + obj = self.doc.getObjectsByLabel("shape-cut")[0] wire = PathOpUtil.offsetWire(getWire(obj.Tool), getNegativeShape(obj), 3, True) self.assertEqual(6, len(wire.Edges)) @@ -587,12 +564,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): if Part.Circle == type(e.Curve): self.assertRoughly(radius, e.Curve.Radius) self.assertCoincide(Vector(0, 0, -1), e.Curve.Axis) - FreeCAD.closeDocument("test_geomop") def test40(self): """Check offsetting a single outside edge forward.""" - doc = FreeCAD.openDocument(DOC) - obj = doc.getObjectsByLabel("offset-edge")[0] + obj = self.doc.getObjectsByLabel("offset-edge")[0] w = getWireOutside(obj) length = 40 * math.cos(math.pi / 6) @@ -628,12 +603,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): self.assertCoincide(Vector(+x, y, 0), wire.Edges[0].Vertexes[0].Point) self.assertCoincide(Vector(-x, y, 0), wire.Edges[0].Vertexes[1].Point) - FreeCAD.closeDocument("test_geomop") def test41(self): """Check offsetting a single outside edge not forward.""" - doc = FreeCAD.openDocument(DOC) - obj = doc.getObjectsByLabel("offset-edge")[0] + obj = self.doc.getObjectsByLabel("offset-edge")[0] w = getWireOutside(obj) length = 40 * math.cos(math.pi / 6) @@ -668,14 +641,12 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): self.assertCoincide(Vector(-x, y, 0), wire.Edges[0].Vertexes[0].Point) self.assertCoincide(Vector(+x, y, 0), wire.Edges[0].Vertexes[1].Point) - FreeCAD.closeDocument("test_geomop") def test42(self): """Check offsetting multiple outside edges.""" - doc = FreeCAD.openDocument(DOC) - obj = doc.getObjectsByLabel("offset-edge")[0] + obj = self.doc.getObjectsByLabel("offset-edge")[0] obj.Shape.tessellate(0.01) - doc.recompute() + self.doc.recompute() w = getWireOutside(obj) length = 40 * math.cos(math.pi / 6) @@ -713,14 +684,12 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): self.assertEqual(1, len(rEdges)) self.assertCoincide(Vector(0, 20, 0), rEdges[0].Curve.Center) self.assertCoincide(Vector(0, 0, +1), rEdges[0].Curve.Axis) - FreeCAD.closeDocument("test_geomop") def test43(self): """Check offsetting multiple backwards outside edges.""" # This is exactly the same as test32, except that the wire is flipped to make # sure the input orientation doesn't matter - doc = FreeCAD.openDocument(DOC) - obj = doc.getObjectsByLabel("offset-edge")[0] + obj = self.doc.getObjectsByLabel("offset-edge")[0] w = getWireOutside(obj) length = 40 * math.cos(math.pi / 6) @@ -759,12 +728,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): self.assertEqual(1, len(rEdges)) self.assertCoincide(Vector(0, 20, 0), rEdges[0].Curve.Center) self.assertCoincide(Vector(0, 0, +1), rEdges[0].Curve.Axis) - FreeCAD.closeDocument("test_geomop") def test44(self): """Check offsetting a single inside edge forward.""" - doc = FreeCAD.openDocument(DOC) - obj = doc.getObjectsByLabel("offset-edge")[0] + obj = self.doc.getObjectsByLabel("offset-edge")[0] w = getWireInside(obj) length = 20 * math.cos(math.pi / 6) @@ -800,12 +767,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): self.assertCoincide(Vector(-x, y, 0), wire.Edges[0].Vertexes[0].Point) self.assertCoincide(Vector(+x, y, 0), wire.Edges[0].Vertexes[1].Point) - FreeCAD.closeDocument("test_geomop") def test45(self): """Check offsetting a single inside edge not forward.""" - doc = FreeCAD.openDocument(DOC) - obj = doc.getObjectsByLabel("offset-edge")[0] + obj = self.doc.getObjectsByLabel("offset-edge")[0] w = getWireInside(obj) length = 20 * math.cos(math.pi / 6) @@ -841,12 +806,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): self.assertCoincide(Vector(+x, y, 0), wire.Edges[0].Vertexes[0].Point) self.assertCoincide(Vector(-x, y, 0), wire.Edges[0].Vertexes[1].Point) - FreeCAD.closeDocument("test_geomop") def test46(self): """Check offsetting multiple inside edges.""" - doc = FreeCAD.openDocument(DOC) - obj = doc.getObjectsByLabel("offset-edge")[0] + obj = self.doc.getObjectsByLabel("offset-edge")[0] w = getWireInside(obj) length = 20 * math.cos(math.pi / 6) @@ -878,14 +841,12 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): rEdges = [e for e in wire.Edges if Part.Circle == type(e.Curve)] self.assertEqual(0, len(rEdges)) - FreeCAD.closeDocument("test_geomop") def test47(self): """Check offsetting multiple backwards inside edges.""" # This is exactly the same as test36 except that the wire is flipped to make # sure it's orientation doesn't matter - doc = FreeCAD.openDocument(DOC) - obj = doc.getObjectsByLabel("offset-edge")[0] + obj = self.doc.getObjectsByLabel("offset-edge")[0] w = getWireInside(obj) length = 20 * math.cos(math.pi / 6) @@ -918,7 +879,6 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): rEdges = [e for e in wire.Edges if Part.Circle == type(e.Curve)] self.assertEqual(0, len(rEdges)) - FreeCAD.closeDocument("test_geomop") def test50(self): """Orient an already oriented wire""" diff --git a/src/Mod/CAM/Tests/TestPathPost.py b/src/Mod/CAM/Tests/TestPathPost.py index ecbb9adcab..9494f5fb6a 100644 --- a/src/Mod/CAM/Tests/TestPathPost.py +++ b/src/Mod/CAM/Tests/TestPathPost.py @@ -21,336 +21,23 @@ # * * # *************************************************************************** +from Path.Post.Command import DlgSelectPostProcessor +from Path.Post.Processor import PostProcessor, PostProcessorFactory +from unittest.mock import patch, MagicMock +import FreeCAD +import Path +import Path.Post.Command as PathCommand +import Path.Post.Processor as PathPost +import Path.Post.Utils as PostUtils import difflib import os import unittest -import FreeCAD -import Path - -import Path.Post.Command as PathPost -import Path.Post.Utils as PostUtils - -from Path.Post.Processor import 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) +PathCommand.LOG_MODULE = Path.Log.thisModule() +Path.Log.setLevel(Path.Log.Level.INFO, PathCommand.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 Tests.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/CAM/Tests" - # - # 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 - - """ - - def setUp(self): - self.testfile = FreeCAD.getHomePath() + "Mod/CAM/Tests/test_filenaming.fcstd" - self.doc = FreeCAD.open(self.testfile) - self.job = self.doc.getObjectsByLabel("MainJob")[0] - - def tearDown(self): - FreeCAD.closeDocument(self.doc.Name) - - 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): +class TestFileNameGenerator(unittest.TestCase): """ String substitution allows the following: @@ -393,18 +80,53 @@ class TestOutputNameSubstitution(unittest.TestCase): """ - def setUp(self): - self.testfile = FreeCAD.getHomePath() + "Mod/CAM/Tests/test_filenaming.fcstd" - self.testfilepath, self.testfilename = os.path.split(self.testfile) - self.testfilename, self.ext = os.path.splitext(self.testfilename) + @classmethod + def setUpClass(cls): + # cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/Tests/boxtest.fcstd") + # cls.job = cls.doc.getObject("Job") - self.doc = FreeCAD.open(self.testfile) - self.job = self.doc.getObjectsByLabel("MainJob")[0] - self.macro = FreeCAD.getUserMacroDir() - self.job.SplitOutput = False + cls.testfile = FreeCAD.getHomePath() + "Mod/CAM/Tests/test_filenaming.fcstd" + cls.testfilepath, cls.testfilename = os.path.split(cls.testfile) + cls.testfilename, cls.ext = os.path.splitext(cls.testfilename) - def tearDown(self): - FreeCAD.closeDocument(self.doc.Name) + cls.doc = FreeCAD.open(cls.testfile) + cls.job = cls.doc.getObjectsByLabel("MainJob")[0] + cls.macro = FreeCAD.getUserMacroDir() + cls.job.SplitOutput = False + + @classmethod + def tearDownClass(cls): + FreeCAD.closeDocument(cls.doc.Name) + + # def test010(self): + # self.job.PostProcessorOutputFile = "" + # generator = PostUtils.FilenameGenerator(job=self.job) + + # filename_generator = generator.generate_filenames() + # generated_filename = next(filename_generator) + # self.assertEqual(generated_filename, "-Job.nc") + + # def test020(self): + # generator = PostUtils.FilenameGenerator(job=self.job) + # filename_generator = generator.generate_filenames() + # expected_filenames = ["-Job.nc"] + [f"-Job-{i}.nc" for i in range(1, 5)] + # print(expected_filenames) + # for expected_filename in expected_filenames: + # generated_filename = next(filename_generator) + # self.assertEqual(generated_filename, expected_filename) + + # def setUp(self): + # self.testfile = FreeCAD.getHomePath() + "Mod/CAM/Tests/test_filenaming.fcstd" + # self.testfilepath, self.testfilename = os.path.split(self.testfile) + # self.testfilename, self.ext = os.path.splitext(self.testfilename) + + # self.doc = FreeCAD.open(self.testfile) + # self.job = self.doc.getObjectsByLabel("MainJob")[0] + # self.macro = FreeCAD.getUserMacroDir() + # self.job.SplitOutput = False + + # def tearDown(self): + # FreeCAD.closeDocument(self.doc.Name) def test000(self): # Test basic name generation with empty string @@ -414,30 +136,19 @@ class TestOutputNameSubstitution(unittest.TestCase): Path.Preferences.setOutputFileDefaults( teststring, "Append Unique ID on conflict" ) - outlist = PathPost.buildPostList(self.job) - self.assertTrue(len(outlist) == 1) - subpart, objs = outlist[0] + generator = PostUtils.FilenameGenerator(job=self.job) + filename_generator = generator.generate_filenames() + filename = next(filename_generator) + Path.Log.debug(filename) + # outlist = PathPost.buildPostList(self.job) - filename = PathPost.resolveFileName(self.job, subpart, 0) - self.assertEqual(filename, f"{self.testfilename}.nc") + # self.assertTrue(len(outlist) == 1) + # subpart, objs = outlist[0] - def test015(self): - # Test basic string substitution without splitting - teststring = "~/Desktop/%j.nc" - self.job.PostProcessorOutputFile = teststring - Path.Preferences.setOutputFileDefaults( - teststring, "Append Unique ID on conflict" - ) - 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") - ) + # filename = PathPost.resolveFileName(self.job, subpart, 0) + #self.assertEqual(filename, os.path.normpath(f"{self.testfilename}.nc")) + self.assertEqual(filename, os.path.join(os.getcwd(), f"{self.testfilename}.nc")) def test010(self): # Substitute current file path @@ -446,24 +157,52 @@ class TestOutputNameSubstitution(unittest.TestCase): Path.Preferences.setOutputFileDefaults( teststring, "Append Unique ID on conflict" ) - outlist = PathPost.buildPostList(self.job) - subpart, objs = outlist[0] - filename = PathPost.resolveFileName(self.job, subpart, 0) + + generator = PostUtils.FilenameGenerator(job=self.job) + filename_generator = generator.generate_filenames() + filename = next(filename_generator) + + print(os.path.normpath(filename)) self.assertEqual( - os.path.normpath(filename), + filename, os.path.normpath(f"{self.testfilepath}/testfile.nc"), ) + def test015(self): + # Test basic string substitution without splitting + teststring = "~/Desktop/%j.nc" + self.job.PostProcessorOutputFile = teststring + Path.Preferences.setOutputFileDefaults( + teststring, "Append Unique ID on conflict" + ) + # outlist = PathPost.buildPostList(self.job) + + # self.assertTrue(len(outlist) == 1) + # subpart, objs = outlist[0] + + generator = PostUtils.FilenameGenerator(job=self.job) + filename_generator = generator.generate_filenames() + filename = next(filename_generator) + + # filename = PathPost.resolveFileName(self.job, subpart, 0) + self.assertEqual( + os.path.normpath(filename), os.path.normpath("~/Desktop/MainJob.nc") + ) + def test020(self): teststring = "%d.nc" self.job.PostProcessorOutputFile = teststring Path.Preferences.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") + + generator = PostUtils.FilenameGenerator(job=self.job) + filename_generator = generator.generate_filenames() + filename = next(filename_generator) + + expected = os.path.join(os.getcwd(), f"{self.testfilename}.nc") + + self.assertEqual(filename, expected ) #f"{self.testfilename}.nc") def test030(self): teststring = "%M/outfile.nc" @@ -471,13 +210,12 @@ class TestOutputNameSubstitution(unittest.TestCase): Path.Preferences.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.macro}outfile.nc") - ) + + generator = PostUtils.FilenameGenerator(job=self.job) + filename_generator = generator.generate_filenames() + filename = next(filename_generator) + + self.assertEqual(filename, os.path.normpath(f"{self.macro}outfile.nc")) def test040(self): # unused substitution strings should be ignored @@ -486,14 +224,38 @@ class TestOutputNameSubstitution(unittest.TestCase): Path.Preferences.setOutputFileDefaults( teststring, "Append Unique ID on conflict" ) - outlist = PathPost.buildPostList(self.job) - subpart, objs = outlist[0] - filename = PathPost.resolveFileName(self.job, subpart, 0) + + generator = PostUtils.FilenameGenerator(job=self.job) + filename_generator = generator.generate_filenames() + filename = next(filename_generator) + self.assertEqual( - os.path.normpath(filename), + filename, os.path.normpath(f"{self.testfilename}/testdoc.nc"), ) + def test045(self): + """Testing the sequence number substitution""" + generator = PostUtils.FilenameGenerator(job=self.job) + filename_generator = generator.generate_filenames() + expected_filenames = [f"test_filenaming{os.sep}testdoc.nc"] + [ + f"test_filenaming{os.sep}testdoc-{i}.nc" for i in range(1, 5) + ] + for expected_filename in expected_filenames: + filename = next(filename_generator) + self.assertEqual(filename, os.path.normpath(expected_filename)) + + def test046(self): + """Testing the sequence number substitution""" + teststring = "%S-%d.nc" + self.job.PostProcessorOutputFile = teststring + generator = PostUtils.FilenameGenerator(job=self.job) + filename_generator = generator.generate_filenames() + expected_filenames = [os.path.join( os.getcwd(), f"{i}-test_filenaming.nc") for i in range(5)] + for expected_filename in expected_filenames: + filename = next(filename_generator) + self.assertEqual(filename, os.path.normpath(expected_filename)) + def test050(self): # explicitly using the sequence number should include it where indicated. teststring = "%S-%d.nc" @@ -501,114 +263,453 @@ class TestOutputNameSubstitution(unittest.TestCase): Path.Preferences.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") + + generator = PostUtils.FilenameGenerator(job=self.job) + filename_generator = generator.generate_filenames() + filename = next(filename_generator) + + self.assertEqual(filename, os.path.join(os.getcwd(), "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" + """Test subpart naming""" + teststring = "%M/outfile.nc" self.job.PostProcessorOutputFile = teststring Path.Preferences.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 - Path.Preferences.setOutputFileDefaults( - teststring, "Append Unique ID on conflict" + generator = PostUtils.FilenameGenerator(job=self.job) + generator.set_subpartname("Tool") + filename_generator = generator.generate_filenames() + filename = next(filename_generator) + + self.assertEqual(filename, os.path.normpath(f"{self.macro}outfile-Tool.nc")) + + +class TestResolvingPostProcessorName(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/Tests/boxtest.fcstd") + cls.job = cls.doc.getObject("Job") + + @classmethod + def tearDownClass(cls): + FreeCAD.closeDocument(cls.doc.Name) + + def setUp(self): + pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/CAM") + pref.SetString("PostProcessorDefault", "") + + def tearDown(self): + pass + + def test010(self): + # Test if post is defined in job + with patch("Path.Post.Processor.PostProcessor.exists", return_value=True): + postname = PathCommand._resolve_post_processor_name(self.job) + self.assertEqual(postname, "linuxcnc") + + def test020(self): + # Test if post is invalid + with patch("Path.Post.Processor.PostProcessor.exists", return_value=False): + with self.assertRaises(ValueError): + PathCommand._resolve_post_processor_name(self.job) + + def test030(self): + # Test if post is defined in prefs + pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/CAM") + pref.SetString("PostProcessorDefault", "grbl") + self.job.PostProcessor = "" + with patch("Path.Post.Processor.PostProcessor.exists", return_value=True): + postname = PathCommand._resolve_post_processor_name(self.job) + self.assertEqual(postname, "grbl") + + def test040(self): + # Test if user interaction is correctly handled + self.job.PostProcessor = "" + if FreeCAD.GuiUp: + with patch("Path.Post.Command.DlgSelectPostProcessor") as mock_dlg, patch( + "Path.Post.Processor.PostProcessor.exists", return_value=True + ): + mock_dlg.return_value.exec_.return_value = "generic" + postname = PathCommand._resolve_post_processor_name(self.job) + self.assertEqual(postname, "generic") + else: + with self.assertRaises(ValueError): + PathCommand._resolve_post_processor_name(self.job) + + +class TestPostProcessorFactory(unittest.TestCase): + """Test creation of postprocessor objects.""" + + @classmethod + def setUpClass(cls): + cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/Tests/boxtest.fcstd") + cls.job = cls.doc.getObject("Job") + + @classmethod + def tearDownClass(cls): + FreeCAD.closeDocument(cls.doc.Name) + + def setUp(self): + pass + + def tearDown(self): + pass + + def test020(self): + # test creation of postprocessor object + post = PostProcessorFactory.get_post_processor(self.job, "generic") + self.assertTrue(post is not None) + self.assertTrue(hasattr(post, "export")) + self.assertTrue(hasattr(post, "_buildPostList")) + + def test030(self): + # test wrapping of old school postprocessor scripts + post = PostProcessorFactory.get_post_processor(self.job, "linuxcnc") + self.assertTrue(post is not None) + self.assertTrue(hasattr(post, "_buildPostList")) + + +class TestPostProcessorClass(unittest.TestCase): + """Test new post structure objects.""" + + @classmethod + def setUpClass(cls): + cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/Tests/boxtest.fcstd") + cls.job = cls.doc.getObject("Job") + + @classmethod + def tearDownClass(cls): + FreeCAD.closeDocument(cls.doc.Name) + + def setUp(self): + pass + + def tearDown(self): + pass + + def test010(self): + """Test the export function.""" + post = PostProcessorFactory.get_post_processor(job, "linuxcnc") + sections = post.export() + for sec in sections: + print(sec[0]) + + def test020(self): + """Test the export function with splitting.""" + post = PostProcessorFactory.get_post_processor(job, "linuxcnc") + sections = post.export() + for sec in sections: + print(sec[0]) + + def test030(self): + """Test the export function with splitting.""" + post = PostProcessorFactory.get_post_processor(job, "generic") + sections = post.export() + for sec in sections: + print(sec[0]) + + +# class TestPostProcessorScript(unittest.TestCase): +# """Test old-school posts""" + +# def setUp(self): +# self.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/Tests/boxtest.fcstd") +# self.job = self.doc.getObject("Job") +# post = PostProcessorFactory.get_post_processor(self.job, "linuxcnc") +# results = post.export() + +# def tearDown(self): +# FreeCAD.closeDocument("boxtest") + +## +## You can run just this test using: +## ./FreeCAD -c -t Tests.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/CAM/Tests" +# # +# # 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. +# self.pp._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 ) - 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 - Path.Preferences.setOutputFileDefaults( - teststring, "Append Unique ID on conflict" + 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 ) - 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 - Path.Preferences.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") +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. - teststring = "%W-%j.nc" - self.job.PostProcessorOutputFile = teststring - Path.Preferences.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") + 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 - def test080(self): - # Split by Operation - self.job.SplitOutput = True + """ + + @classmethod + def setUpClass(cls): + cls.testfile = FreeCAD.getHomePath() + "Mod/CAM/Tests/test_filenaming.fcstd" + cls.doc = FreeCAD.open(cls.testfile) + cls.job = cls.doc.getObjectsByLabel("MainJob")[0] + + @classmethod + def tearDownClass(cls): + FreeCAD.closeDocument(cls.doc.Name) + + def setUp(self): + self.pp = PathPost.PostProcessor(self.job, "generic", "", "") + + 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" - outlist = PathPost.buildPostList(self.job) - teststring = "%j.nc" - self.job.PostProcessorOutputFile = teststring - Path.Preferences.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") + def test010(self): + postlist = self.pp._buildPostList() - teststring = "%O-%j.nc" + 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 = self.pp._buildPostList() + 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 = self.pp._buildPostList() + 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 - Path.Preferences.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") + self.job.OrderOutputBy = "Tool" + postlist = self.pp._buildPostList() + + 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 = self.pp._buildPostList() + + 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 = self.pp._buildPostList() + + firstoutputitem = postlist[0] + firstoplist = firstoutputitem[1] + self.assertTrue(len(firstoplist) == 6) + self.assertTrue(firstoutputitem[0] == "G54")