Files
create/src/Mod/Path/PathTests/TestPathPost.py
2022-10-11 21:43:08 -07:00

612 lines
23 KiB
Python

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