# *************************************************************************** # * Copyright (c) 2018 Bernd Hahnebach * # * * # * This file is part of the FreeCAD CAx development system. * # * * # * 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 * # * * # *************************************************************************** __title__ = "Tools for FEM unit tests" __author__ = "Bernd Hahnebach" __url__ = "https://www.freecad.org" import math import os import sys import tempfile import unittest from typing import Union, List, Iterator import FreeCAD from os.path import join def get_fem_test_home_dir(): return join(FreeCAD.getHomePath(), "Mod", "Fem", "femtest", "data") def get_fem_test_tmp_dir(dirname=None): from uuid import uuid4 _unique_id = str(uuid4())[-12:] # print(_unique_id) if dirname is None: temp_dir = join(tempfile.gettempdir(), "FEM_unittests", _unique_id) else: temp_dir = join(tempfile.gettempdir(), "FEM_unittests", dirname + "_" + _unique_id) if not os.path.exists(temp_dir): os.makedirs(temp_dir) return temp_dir def get_unit_test_tmp_dir(temp_dir, unittestdir): testdir = join(temp_dir, unittestdir) if not os.path.exists(testdir): os.makedirs(testdir) return testdir def fcc_print(message): FreeCAD.Console.PrintMessage(f"{message} \n") def get_namefromdef(strdel="", stradd=""): # https://code.activestate.com/recipes/66062-determining-current-function-name/ return (sys._getframe(1).f_code.co_name).replace(strdel, stradd) def get_defmake_count(fem_vtk_post=True): """ count the def make in module ObjectsFem could also be done in bash with grep -c "def make" src/Mod/Fem/ObjectsFem.py """ name_modfile = join(FreeCAD.getHomePath(), "Mod", "Fem", "ObjectsFem.py") modfile = open(name_modfile) lines_modefile = modfile.readlines() modfile.close() lines_defmake = [li for li in lines_modefile if li.startswith("def make")] if not fem_vtk_post: # FEM VTK post processing is disabled # we are not able to create VTK post objects new_lines = [] for li in lines_defmake: if "Post" not in li: new_lines.append(li) lines_defmake = new_lines return len(lines_defmake) def get_fem_test_defs(): test_path = join(FreeCAD.getHomePath(), "Mod", "Fem", "femtest", "app") print(f"Modules, classes, methods taken from: {test_path}") collected_test_module_paths = [] for tfile in sorted(os.listdir(test_path)): if tfile.startswith("test") and tfile.endswith(".py"): collected_test_module_paths.append(join(test_path, tfile)) collected_test_modules = [] collected_test_classes = [] collected_test_methods = [] for f in collected_test_module_paths: module_name = os.path.splitext(os.path.basename(f))[0] module_path = f"femtest.app.{module_name}" if module_path not in collected_test_modules: collected_test_modules.append(module_path) class_name = "" tfile = open(f) for ln in tfile: ln = ln.lstrip() ln = ln.rstrip() if ln.startswith("class "): ln = ln.lstrip("class ") ln = ln.split("(")[0] class_name = ln class_path = f"femtest.app.{module_name}.{class_name}" if class_path not in collected_test_classes: collected_test_classes.append(class_path) if ln.startswith("def test"): ln = ln.lstrip("def ") ln = ln.split("(")[0] if ln == "test_00print": continue method_path = f"femtest.app.{module_name}.{class_name}.{ln}" collected_test_methods.append(method_path) tfile.close() # write to file file_path = join(tempfile.gettempdir(), "test_commands.sh") cf = open(file_path, "w") cf.write("# created by Python\n") cf.write("'''\n") cf.write("from femtest.app.support_utils import get_fem_test_defs\n") cf.write("get_fem_test_defs()\n") cf.write("\n") cf.write("\n") cf.write("# all FEM App tests\n") cf.write("make -j 4 && ./bin/FreeCAD --run-test 'TestFemApp'\n") cf.write("\n") cf.write("make -j 4 && ./bin/FreeCADCmd --run-test 'TestFemApp'\n") cf.write("\n") cf.write("\n") cf.write("'''\n") cf.write("\n") cf.write("# modules\n") for m in collected_test_modules: cf.write(f"make -j 4 && ./bin/FreeCADCmd -t {m}\n") cf.write("\n") cf.write("\n") cf.write("# classes\n") for m in collected_test_classes: cf.write(f"make -j 4 && ./bin/FreeCADCmd -t {m}\n") cf.write("\n") cf.write("\n") cf.write("# methods\n") for m in collected_test_methods: cf.write(f"make -j 4 && ./bin/FreeCADCmd -t {m}\n") cf.write("\n") cf.write("\n") cf.write("# methods in FreeCAD\n") for m in collected_test_methods: cf.write( "\nimport unittest\n" "unittest.TextTestRunner().run(unittest.TestLoader().loadTestsFromName(\n" " '{}'\n" "))\n".format(m) ) cf.close() print(f"The file was saved in:{file_path}") def try_converting_to_float(line: str) -> Union[None, List[float]]: """Does its best to split a line and convert its elements to float Has 3 strategies of splitting: * by comma - mainly in CalculiX .inp files * by space - other solvers * by space and ignoring the 1st word - other solvers If there was only 1 word, the line will fail, no compromises Single characters always pass :param line: line to split and convert to floats :return: None if conversion failed, else list of floats """ strategies = [ lambda _line: _line[1:].split(","), lambda _line: _line[1:].split(), lambda _line: (lambda split_line: len(split_line) > 1 and split_line or "fail")( _line[1:].split()[1:] ), ] for strategy in strategies: try: return list(map(float, strategy(line))) except ValueError: pass return None def are_floats_equal( orig_floats: Union[None, List[float]], floats_to_compare: Union[None, List[float]] ) -> bool: """Check if floats in lists are equal with some tolerance :param orig_floats: list of floats - left operands of comparison :param floats_to_compare: list of floats - right operands of comparison :return: True if both lists are equal with some tolerance element-wise, else False """ if any(floats is None for floats in (orig_floats, floats_to_compare)): return False for orig_float, float_to_compare in zip(orig_floats, floats_to_compare): if not math.isclose(orig_float, float_to_compare, abs_tol=1e-10): return False return True def parse_diff(diff_lines: Iterator[str]) -> List[str]: """Parses lines from `united_diff` Tries to split a line and convert its contents to float, to compare float numbers with some tolerance Recognizes blocks of changes, that start with `@@` and end either at EOF or at the beginning of another block. Only bad lines are added to block. All lines, which didn't pass the element-wise check, are bad lines. :param diff_lines: lines produced by `united_diff` :return: list of bad lines with respect to their block of change """ bad_lines = [] changes_block = [] while True: try: first = next(diff_lines) if first.startswith("@@"): if len(changes_block) > 1: bad_lines.extend(changes_block) changes_block = [first] continue second = next(diff_lines) except StopIteration: break if first.startswith("---"): continue if not are_floats_equal(*map(try_converting_to_float, (first, second))): changes_block.extend((first, second)) # check if we have any remaining block if len(changes_block) > 1: bad_lines.extend(changes_block) return bad_lines def compare_inp_files(file_name1, file_name2): file1 = open(file_name1) f1 = file1.readlines() file1.close() # l.startswith("17671.0,1") is a temporary workaround # for python3 problem with 1DFlow input # TODO as soon as the 1DFlow result reading is fixed # this should be triggered in the 1DFlow unit test lf1 = [ li for li in f1 if not ( li.startswith("** written ") or li.startswith("** file ") or li.startswith("17671.0,1") ) ] lf1 = force_unix_line_ends(lf1) file2 = open(file_name2) f2 = file2.readlines() file2.close() # TODO see comment on file1 lf2 = [ li for li in f2 if not ( li.startswith("** written ") or li.startswith("** file ") or li.startswith("17671.0,1") ) ] lf2 = force_unix_line_ends(lf2) import difflib diff_lines = difflib.unified_diff(lf1, lf2, n=0) bad_lines = parse_diff(diff_lines) if bad_lines: return f"Comparing {file_name1} to {file_name2} failed!\n{''.join(bad_lines)}" def compare_files(file_name1, file_name2): file1 = open(file_name1) f1 = file1.readlines() file1.close() # TODO: add support for variable values in the reference file # instead of using this workaround # workaround to compare geos of elmer test and temporary file path # (not only names change, path changes with operating system) lf1 = [ li for li in f1 if not ( li.startswith('Merge "') or li.startswith('Save "') or li.startswith("// ") or li.startswith("General.NumThreads") ) ] lf1 = force_unix_line_ends(lf1) file2 = open(file_name2) f2 = file2.readlines() file2.close() lf2 = [ li for li in f2 if not ( li.startswith('Merge "') or li.startswith('Save "') or li.startswith("// ") or li.startswith("General.NumThreads") ) ] lf2 = force_unix_line_ends(lf2) import difflib diff = difflib.unified_diff(lf1, lf2, n=0) result = "" for li in diff: result += li if result: result = f"Comparing {file_name1} to {file_name2} failed!\n" + result return result def compare_stats(fea, stat_file, res_obj_name, loc_stat_types=None): import femresult.resulttools as resulttools # get the stat types which should be compared stat_types = [ "U1", "U2", "U3", "Uabs", "Sabs", "MaxPrin", "MidPrin", "MinPrin", "MaxShear", "Peeq", "Temp", "MFlow", "NPress", ] if not loc_stat_types: loc_stat_types = stat_types # get stats from result obj which should be compared obj = fea.analysis.Document.getObject(res_obj_name) # fcc_print(obj) if obj: # fcc_print(obj.Name) stats = [] for s in loc_stat_types: statval = resulttools.get_stats(obj, s) stats.append(f"{s}: ({statval[0]:.10f}, {statval[1]:.10f})\n") else: fcc_print(f"Result object not found. Name: {res_obj_name}") return True # get stats to compare with, the expected ones sf = open(stat_file) sf_content = [] for li in sf.readlines(): for st in loc_stat_types: if li.startswith(st): sf_content.append(li) sf.close() sf_content = force_unix_line_ends(sf_content) if sf_content == []: return True # compare stats if stats != sf_content: fcc_print(f"Stats read from {fea.base_name}.frd file") fcc_print("!=") fcc_print(f"Expected stats from {stat_file}") for i in range(len(stats)): if stats[i] != sf_content[i]: fcc_print(f"{stats[i].rstrip()} != {sf_content[i].rstrip()}") return True return False def force_unix_line_ends(line_list): new_line_list = [] for ln in line_list: if ln.endswith("\r\n"): ln = ln[:-2] + "\n" new_line_list.append(ln) return new_line_list def collect_python_modules(femsubdir=None): if not femsubdir: pydir = join(FreeCAD.ConfigGet("AppHomePath"), "Mod", "Fem") else: pydir = join(FreeCAD.ConfigGet("AppHomePath"), "Mod", "Fem", femsubdir) collected_modules = [] fcc_print(pydir) for pyfile in sorted(os.listdir(pydir)): if pyfile.endswith(".py") and not pyfile.startswith("Init"): if not femsubdir: collected_modules.append(os.path.splitext(os.path.basename(pyfile))[0]) else: collected_modules.append( femsubdir.replace("/", ".") + "." + os.path.splitext(os.path.basename(pyfile))[0] ) return collected_modules def all_test_files(): # open all files cube_frequency() cube_static() Flow1D_thermomech() multimat() spine_thermomech() # run the specific test case of the file # open the file in FreeCAD GUI and return the doc identifier def cube_frequency(): testname = "femtest.testccxtools.TestCcxTools.test_3_freq_analysis" unittest.TextTestRunner().run(unittest.TestLoader().loadTestsFromName(testname)) doc = FreeCAD.open(join(get_fem_test_tmp_dir(), "FEM_ccx_frequency", "cube_frequency.FCStd")) return doc def cube_static(): testname = "femtest.testccxtools.TestCcxTools.test_1_static_analysis" unittest.TextTestRunner().run(unittest.TestLoader().loadTestsFromName(testname)) doc = FreeCAD.open(join(get_fem_test_tmp_dir(), "FEM_ccx_static", "cube_static.FCStd")) return doc def Flow1D_thermomech(): testname = "femtest.testccxtools.TestCcxTools.test_5_Flow1D_thermomech_analysis" unittest.TextTestRunner().run(unittest.TestLoader().loadTestsFromName(testname)) doc = FreeCAD.open( join(get_fem_test_tmp_dir(), "FEM_ccx_Flow1D_thermomech", "Flow1D_thermomech.FCStd") ) return doc def multimat(): testname = "femtest.testccxtools.TestCcxTools.test_2_static_multiple_material" unittest.TextTestRunner().run(unittest.TestLoader().loadTestsFromName(testname)) doc = FreeCAD.open(join(get_fem_test_tmp_dir(), "FEM_ccx_multimat", "multimat.FCStd")) return doc def spine_thermomech(): testname = "femtest.testccxtools.TestCcxTools.test_4_thermomech_analysis" unittest.TextTestRunner().run(unittest.TestLoader().loadTestsFromName(testname)) doc = FreeCAD.open(join(get_fem_test_tmp_dir(), "FEM_ccx_thermomech", "spine_thermomech.FCStd")) return doc