diff --git a/src/ifcbimtester/__init__.py b/src/ifcbimtester/__init__.py new file mode 100644 index 0000000000..866cf19fa6 --- /dev/null +++ b/src/ifcbimtester/__init__.py @@ -0,0 +1,28 @@ +# *************************************************************************** +# * Copyright (c) 2020 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 * +# * * +# *************************************************************************** + +from os.path import dirname +from os.path import realpath + + +code_bimtester_path = dirname(realpath(__file__)) diff --git a/src/ifcbimtester/bimtester.py b/src/ifcbimtester/bimtester.py old mode 100755 new mode 100644 index d4a0a3ae43..40be8598b6 --- a/src/ifcbimtester/bimtester.py +++ b/src/ifcbimtester/bimtester.py @@ -1,210 +1,48 @@ #!/usr/bin/env python3 + +# *************************************************************************** +# * Copyright (c) 2020 Dion Moult <> * +# * Copyright (c) 2020 Bernd Hahnebach * +# * * +# * 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 * +# * * +# *************************************************************************** + +# Bernd: IMHO, this should go one lever up, +# or all other code should go one level down +# https://stackoverflow.com/questions/16981921/relative-imports-in-python-3 + + +# ATM bimtester code is copied here for for the sake of convenience +# TODO bimtester should be installed in conjunction with ifcopenshell +# to /urs/local by make install + + # Unix: # $ pyinstaller --onefile --clean --icon=icon.ico --add-data "features:features" bimtester.py` # Windows: # $ pyinstaller --onefile --clean --icon=icon.ico --add-data "features;features" bimtester.py` -from behave.__main__ import main as behave_main -import behave.formatter.pretty # Needed for pyinstaller to package it -import ifcopenshell -import pystache -import os -import sys -import json + import argparse -import csv -import re -import shutil -import webbrowser -import datetime -from pathlib import Path -try: - # PyInstaller creates a temp folder and stores path in _MEIPASS - base_path = sys._MEIPASS -except Exception: - base_path = os.path.dirname(os.path.realpath(__file__)) - - -def get_resource_path(relative_path): - return os.path.join(base_path, relative_path) - - -def run_tests(args): - if not get_features(args): - print("No features could be found to check.") - return False - behave_args = [get_resource_path("features")] - if args["advanced_arguments"]: - behave_args.extend(args["advanced_arguments"].split()) - elif not args["console"]: - behave_args.extend(["--format", "json.pretty", "--outfile", "report/report.json"]) - behave_main(behave_args) - print("# All tests are finished.") - return True - - -def get_features(args): - current_path = os.path.abspath(".") - features_dir = get_resource_path("features") - for f in os.listdir(features_dir): - if f.endswith(".feature"): - os.remove(os.path.join(features_dir, f)) - if args["feature"]: - shutil.copyfile(args["feature"], os.path.join(get_resource_path("features"), os.path.basename(args["feature"]))) - return True - if os.path.exists("features"): - shutil.copytree("features", get_resource_path("features")) - return True - has_features = False - for f in os.listdir("."): - if not f.endswith(".feature"): - continue - if args["feature"] and args["feature"] != f: - continue - has_features = True - shutil.copyfile(f, os.path.join(get_resource_path("features"), os.path.basename(f))) - return has_features - - -def generate_report(adir="."): - print("# Generating HTML reports now.") - - # get html template - html_template_file = os.path.join( - os.path.dirname(os.path.realpath(__file__)), - "features/template.html" - ) - - # get report file - report_dir = os.path.join(adir, "report") - if not os.path.exists(report_dir): - return print("No report directory was found.") - report_path = os.path.join(report_dir, "report.json") - # print(report_path) - if not os.path.exists(report_path): - return print("No report data was found.") - - # read json report and create html report for each feature - report = json.loads(open(report_path).read()) - for feature in report: - file_name = os.path.basename(feature["location"]).split(":")[0] - data = { - "file_name": file_name, - "time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "name": feature["name"], - "description": feature["description"], - "is_success": feature["status"] == "passed", - "scenarios": [], - } - if "elements" not in feature: - if "status" in feature and feature["status"] == "skipped": - print("Feature was skipped. No html report will be created.") - else: - print("For a unknown reason no html report well be created.") - # happens if the feature file does not consist of any valid Scenario - continue - for scenario in feature["elements"]: - steps = [] - total_duration = 0 - if len(scenario["steps"]) == 0: - print( - "Scenario '{}' in feature '{}' has no steps. " - "Thus skipped in report." - .format(scenario["name"], feature["name"]) - ) - continue - for step in scenario["steps"]: - if "result" in step: - total_duration += step["result"]["duration"] - name = step["name"] - if "match" in step and "arguments" in step["match"]: - for a in step["match"]["arguments"]: - name = name.replace(a["value"], "" + a["value"] + "") - if "result" not in step or step["result"]["status"] == "undefined": - step["result"] = {} - step["result"]["status"] = "undefined" - step["result"]["duration"] = 0 - step["result"]["error_message"] = "This requirement has not yet been specified." - steps.append( - { - "name": name, - "time": round(step["result"]["duration"], 2), - "is_success": step["result"]["status"] == "passed", - "is_unspecified": "result" not in step or step["result"]["status"] == "undefined", - "error_message": None - if step["result"]["status"] == "passed" - else step["result"]["error_message"], - } - ) - total_passes = len([s for s in steps if s["is_success"] == True]) - total_steps = len(steps) - pass_rate = round((total_passes / total_steps) * 100) - data["scenarios"].append( - { - "name": scenario["name"], - # on behave < 1.2.6 there is no 'status' thus report fails - "is_success": scenario["status"] == "passed", - "time": round(total_duration, 2), - "steps": steps, - "total_passes": total_passes, - "total_steps": total_steps, - "pass_rate": pass_rate, - } - ) - data["total_passes"] = sum([s["total_passes"] for s in data["scenarios"]]) - data["total_steps"] = sum([s["total_steps"] for s in data["scenarios"]]) - data["pass_rate"] = round((data["total_passes"] / data["total_steps"]) * 100) - - # create the html file - html_report_file = os.path.join(report_dir, "{}.html".format(file_name)) - with open(html_report_file, "w") as out: - with open(html_template_file) as template: - out.write(pystache.render(template.read(), data)) - - -class TestPurger: - def __init__(self): - self.file = None - - def purge(self): - filenames = [] - if os.path.exists("features"): - for filename in Path("features/").glob("*.feature"): - filenames.append(filename) - for f in os.listdir("."): - if f.endswith(".feature"): - filenames.append(f) - - for filename in filenames: - with open(filename, "r") as feature_file: - old_file = feature_file.readlines() - with open(filename, "w") as new_file: - for line in old_file: - is_purged = False - if 'The IFC file "' in line and '" must be provided' in line: - filename = line.split('"')[1] - print("Loading file {} ...".format(filename)) - self.file = ifcopenshell.open(filename) - if line.strip()[0:2] == "* ": - words = line.strip().split() - for word in words: - if self.is_a_global_id(word): - if not self.does_global_id_exist(word): - print("Test for {} purged ...".format(word)) - is_purged = True - if not is_purged: - new_file.write(line) - - def is_a_global_id(self, word): - return word[0] in ["0", "1", "2", "3"] and len(word) == 22 - - def does_global_id_exist(self, global_id): - try: - self.file.by_guid(global_id) - return True - except: - return False +import clean +import reports +import run if __name__ == "__main__": @@ -219,9 +57,9 @@ if __name__ == "__main__": args = vars(parser.parse_args()) if args["purge"]: - TestPurger().purge() + clean.TestPurger().purge() elif args["report"]: - generate_report() + reports.generate_report() else: - run_tests(args) + run.run_tests(args) print("# All tasks are complete :-)") diff --git a/src/ifcbimtester/clean.py b/src/ifcbimtester/clean.py new file mode 100644 index 0000000000..2efe7d4f09 --- /dev/null +++ b/src/ifcbimtester/clean.py @@ -0,0 +1,68 @@ +# *************************************************************************** +# * Copyright (c) 2020 Dion Moult <> * +# * * +# * 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 ifcopenshell +import os +from pathlib import Path + + +class TestPurger: + def __init__(self): + self.file = None + + def purge(self): + filenames = [] + if os.path.exists("features"): + for filename in Path("features/").glob("*.feature"): + filenames.append(filename) + for f in os.listdir("."): + if f.endswith(".feature"): + filenames.append(f) + + for filename in filenames: + with open(filename, "r") as feature_file: + old_file = feature_file.readlines() + with open(filename, "w") as new_file: + for line in old_file: + is_purged = False + if 'The IFC file "' in line and '" must be provided' in line: + filename = line.split('"')[1] + print("Loading file {} ...".format(filename)) + self.file = ifcopenshell.open(filename) + if line.strip()[0:2] == "* ": + words = line.strip().split() + for word in words: + if self.is_a_global_id(word): + if not self.does_global_id_exist(word): + print("Test for {} purged ...".format(word)) + is_purged = True + if not is_purged: + new_file.write(line) + + def is_a_global_id(self, word): + return word[0] in ["0", "1", "2", "3"] and len(word) == 22 + + def does_global_id_exist(self, global_id): + try: + self.file.by_guid(global_id) + return True + except Exception: + return False diff --git a/src/ifcbimtester/guiwidget.py b/src/ifcbimtester/guiwidget.py new file mode 100644 index 0000000000..9fe2962e21 --- /dev/null +++ b/src/ifcbimtester/guiwidget.py @@ -0,0 +1,231 @@ +# *************************************************************************** +# * Copyright (c) 2020 Bernd Hahnebach * +# * * +# * 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 * +# * * +# *************************************************************************** + +# TODO: improve layout, start with feature file path and beside button !!!!! +# TODO: if browse widgets will be canceled, last QLineEdit should be restored + +import os + +from PySide2 import QtCore +from PySide2 import QtGui +from PySide2 import QtWidgets + +from .run import run_all + + +class GuiWidgetBimTester(QtWidgets.QWidget): + + # get some initial values + initial_ifcfile = "/home/hugo/Documents/zeug_sort/z_some_ifc/example_model.ifc" + if not os.path.isfile(initial_ifcfile): + initial_ifcfile = os.path.join(os.path.expanduser("~"), "Desktop") + this_path = os.path.join(os.path.dirname(__file__)) + initial_featurespath = os.path.join(this_path, "..", "features_bimtester", "fea_min") + + def __init__(self): + super(GuiWidgetBimTester, self).__init__() + self._setup_ui() + + def __del__(self,): + # need as fix for qt event error + # http://forum.freecadweb.org/viewtopic.php?f=18&t=10732&start=10#p86493 + return + + def _setup_ui(self): + + # a lot code is taken from FreeCAD FEM solver frame work task panel + # https://forum.freecadweb.org/viewtopic.php?f=10&t=51419 + # use a browse button, and a line edit + # the browse button opens a file dialog, which will set the line edit + + # icon + # print(__file__) + package_path = os.path.dirname(os.path.realpath(__file__)) + iconpath = os.path.join( + package_path, "resources", "icons", "bimtester.ico" + ) + """ + # as svg + # https://stackoverflow.com/a/35138314 + theicon = QtSvg.QSvgWidget(iconpath) + # none works ... + #theicon.setGeometry(20,20,200,200) + #theicon.setSizePolicy(QtGui.QSizePolicy.Policy.Maximum, QtGui.QSizePolicy.Policy.Maximum) + #theicon.sizeHint() + """ + # as pixmap + theicon = QtWidgets.QLabel(self) + iconpixmap = QtGui.QPixmap(iconpath) + iconpixmap = iconpixmap.scaled(100, 100, QtCore.Qt.KeepAspectRatio) + theicon.setPixmap(iconpixmap) + + # ifc file + _ifcfile_label = QtWidgets.QLabel("IFC file", self) + self.ifcfile_text = QtWidgets.QLineEdit() + self.set_ifcfile(self.initial_ifcfile) + _ifcfile_browse_btn = QtWidgets.QToolButton() + _ifcfile_browse_btn.setText("...") + _ifcfile_browse_btn.clicked.connect(self.select_ifcfile) + + # feature files path + # use a layout with a frame and a title, see solver framework tp + # beside button + ffifc_str = ( + "Feature files beside IFC file. " + "Feature files in directory features." + ) + featuredirfromifc_label = QtWidgets.QLabel(ffifc_str, self) + self.featuredirfromifc_cb = QtWidgets.QCheckBox(self) + self.featuredirfromifc_cb.stateChanged.connect( + self.featuredirfromifc_clicked + ) + + # path browser and line edit + _ffdir_str = ( + "Feature files directory. " + "Feature files in directory features." + ) + _featurefilesdir_label = QtWidgets.QLabel(_ffdir_str, self) + self.featurefilesdir_text = QtWidgets.QLineEdit() + self.set_featurefilesdir(self.initial_featurespath) + self.feafilesdir_browse_btn = QtWidgets.QToolButton() + self.feafilesdir_browse_btn.setText("...") + self.feafilesdir_browse_btn.clicked.connect( + self.select_featurefilesdir + ) + + # buttons + self.run_button = QtWidgets.QPushButton( + QtGui.QIcon.fromTheme("document-new"), "Run" + ) + self.close_button = QtWidgets.QPushButton( + QtGui.QIcon.fromTheme("window-close"), "Close" + ) + self.run_button.clicked.connect(self.run_bimtester) + self.close_button.clicked.connect(self.close_widget) + _buttons = QtWidgets.QHBoxLayout() + _buttons.addWidget(self.run_button) + _buttons.addWidget(self.close_button) + + # Layout: + layout = QtWidgets.QGridLayout() + layout.addWidget(theicon, 1, 0, alignment=QtCore.Qt.AlignRight) + layout.addWidget(_ifcfile_label, 2, 0) + layout.addWidget(self.ifcfile_text, 3, 0) + layout.addWidget(_ifcfile_browse_btn, 3, 1) + layout.addWidget(_featurefilesdir_label, 4, 0) + layout.addWidget(self.featurefilesdir_text, 5, 0) + layout.addWidget(self.feafilesdir_browse_btn, 5, 1) + layout.addWidget(featuredirfromifc_label, 6, 0) + layout.addWidget(self.featuredirfromifc_cb, 6, 1) + layout.addLayout(_buttons, 7, 0) + # row stretches by 10 compared to the others, std is 0 + # first parameter is the row number + # second is the stretch factor. + layout.setRowStretch(0, 10) + self.setLayout(layout) + + # ********************************************************** + def select_ifcfile(self): + # print(self.get_ifcfile()) + # print(os.path.isfile(self.get_ifcfile())) + ifcfile = QtWidgets.QFileDialog.getOpenFileName( + self, + dir=self.get_ifcfile() + )[0] + self.set_ifcfile(ifcfile) + + def set_ifcfile(self, a_file): + self.ifcfile_text.setText(a_file) + + def get_ifcfile(self): + return self.ifcfile_text.text() + + def featuredirfromifc_clicked(self): + if self.featuredirfromifc_cb.isChecked() is True: + self.set_featurefilesdir("") + # TODO + self.featurefilesdir_text.setEnabled(False) + self.feafilesdir_browse_btn.setEnabled(False) + # deactivate feature path browser button + # deactivate lineedit text + else: + self.set_featurefilesdir(self.initial_featurespath) + self.featurefilesdir_text.setEnabled(True) + self.feafilesdir_browse_btn.setEnabled(True) + + def select_featurefilesdir(self): + thedir = self.featurefilesdir_text.text() + # print(thedir) + # print(os.path.isdir(thedir)) + # hidden directories are only shown if the option is set + features_path = QtWidgets.QFileDialog.getExistingDirectory( + self, + caption="Choose features directory ...", + dir=thedir, + options=QtWidgets.QFileDialog.HideNameFilterDetails + ) + self.set_featurefilesdir(features_path) + + def set_featurefilesdir(self, a_directory): + self.featurefilesdir_text.setText(a_directory) + + def get_featurefilesdir(self): + return self.featurefilesdir_text.text() + + # ********************************************************** + def run_bimtester(self): + print("Run BIMTester") + QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) + + # get input values + splitifcpath = os.path.split(self.get_ifcfile()) + the_ifcfile_path, the_ifcfile_name = splitifcpath[0], splitifcpath[1] + if self.featuredirfromifc_cb.isChecked() is True: + the_features_path = the_ifcfile_path + print( + "Make sure the feature files are beside " + "the ifc file in a directory named 'features'." + ) + else: + the_features_path = self.get_featurefilesdir() + print(the_features_path) + print(the_ifcfile_path) + print(the_ifcfile_name) + + # run bimtester + status = run_all( + the_features_path, + the_ifcfile_path, + the_ifcfile_name + ) + print(status) + + QtWidgets.QApplication.restoreOverrideCursor() + + def close_widget(self): + print("Close BIMTester Gui") + self.close() + + def closeEvent(self, ev): + pw = self.parentWidget() + if pw and pw.inherits("QDockWidget"): + pw.deleteLater() diff --git a/src/ifcbimtester/reports.py b/src/ifcbimtester/reports.py new file mode 100644 index 0000000000..74f7482176 --- /dev/null +++ b/src/ifcbimtester/reports.py @@ -0,0 +1,121 @@ +# *************************************************************************** +# * Copyright (c) 2020 Dion Moult <> * +# * Copyright (c) 2020 Bernd Hahnebach * +# * * +# * 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 datetime +import json +import os +import pystache + + +def generate_report(adir="."): + print("# Generating HTML reports now.") + + # get html template + html_template_file = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + "features/template.html" + ) + + # get report file + report_dir = os.path.join(adir, "report") + if not os.path.exists(report_dir): + return print("No report directory was found.") + report_path = os.path.join(report_dir, "report.json") + # print(report_path) + if not os.path.exists(report_path): + return print("No report data was found.") + + # read json report and create html report for each feature + report = json.loads(open(report_path).read()) + for feature in report: + file_name = os.path.basename(feature["location"]).split(":")[0] + data = { + "file_name": file_name, + "time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "name": feature["name"], + "description": feature["description"], + "is_success": feature["status"] == "passed", + "scenarios": [], + } + if "elements" not in feature: + if "status" in feature and feature["status"] == "skipped": + print("Feature was skipped. No html report will be created.") + else: + print("For a unknown reason no html report well be created.") + # happens if the feature file does not consist of any valid Scenario + continue + for scenario in feature["elements"]: + steps = [] + total_duration = 0 + if len(scenario["steps"]) == 0: + print( + "Scenario '{}' in feature '{}' has no steps. " + "Thus skipped in report." + .format(scenario["name"], feature["name"]) + ) + continue + for step in scenario["steps"]: + if "result" in step: + total_duration += step["result"]["duration"] + name = step["name"] + if "match" in step and "arguments" in step["match"]: + for a in step["match"]["arguments"]: + name = name.replace(a["value"], "" + a["value"] + "") + if "result" not in step or step["result"]["status"] == "undefined": + step["result"] = {} + step["result"]["status"] = "undefined" + step["result"]["duration"] = 0 + step["result"]["error_message"] = "This requirement has not yet been specified." + steps.append( + { + "name": name, + "time": round(step["result"]["duration"], 2), + "is_success": step["result"]["status"] == "passed", + "is_unspecified": "result" not in step or step["result"]["status"] == "undefined", + "error_message": None + if step["result"]["status"] == "passed" + else step["result"]["error_message"], + } + ) + total_passes = len([s for s in steps if s["is_success"] is True]) + total_steps = len(steps) + pass_rate = round((total_passes / total_steps) * 100) + data["scenarios"].append( + { + "name": scenario["name"], + # on behave < 1.2.6 there is no 'status' thus report fails + "is_success": scenario["status"] == "passed", + "time": round(total_duration, 2), + "steps": steps, + "total_passes": total_passes, + "total_steps": total_steps, + "pass_rate": pass_rate, + } + ) + data["total_passes"] = sum([s["total_passes"] for s in data["scenarios"]]) + data["total_steps"] = sum([s["total_steps"] for s in data["scenarios"]]) + data["pass_rate"] = round((data["total_passes"] / data["total_steps"]) * 100) + + html_report_file = os.path.join(report_dir, "{}.html".format(file_name)) + with open(html_report_file, "w") as out: + with open(html_template_file) as template: + out.write(pystache.render(template.read(), data)) diff --git a/src/ifcbimtester/icon.ico b/src/ifcbimtester/resources/icons/bimtester.ico similarity index 100% rename from src/ifcbimtester/icon.ico rename to src/ifcbimtester/resources/icons/bimtester.ico diff --git a/src/ifcbimtester/run.py b/src/ifcbimtester/run.py new file mode 100644 index 0000000000..57f73a1c71 --- /dev/null +++ b/src/ifcbimtester/run.py @@ -0,0 +1,295 @@ +# *************************************************************************** +# * Copyright (c) 2020 Dion Moult <> * +# * Copyright (c) 2020 Bernd Hahnebach * +# * * +# * 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 * +# * * +# *************************************************************************** + +from behave.__main__ import main as behave_main +import behave.formatter.pretty # Needed for pyinstaller to package it +import os +import sys +import shutil +import webbrowser +import fileinput +import tempfile + + +# get bimtester source code module path +bimtester_path = os.path.dirname(os.path.realpath(__file__)) +# print(bimtester_path) +try: + # PyInstaller creates a temp folder and stores path in _MEIPASS + base_path = sys._MEIPASS +except Exception: + base_path = os.path.dirname(os.path.realpath(__file__)) + + +def get_resource_path(relative_path): + return os.path.join(base_path, relative_path) + + +def run_tests(args): + if not get_features(args): + print("No features could be found to check.") + return False + behave_args = [get_resource_path("features")] + if args["advanced_arguments"]: + behave_args.extend(args["advanced_arguments"].split()) + elif not args["console"]: + behave_args.extend(["--format", "json.pretty", "--outfile", "report/report.json"]) + behave_main(behave_args) + print("# All tests are finished.") + return True + + +def get_features(args): + # current_path = os.path.abspath(".") + features_dir = get_resource_path("features") + for f in os.listdir(features_dir): + if f.endswith(".feature"): + os.remove(os.path.join(features_dir, f)) + if args["feature"]: + shutil.copyfile(args["feature"], os.path.join(get_resource_path("features"), os.path.basename(args["feature"]))) + return True + if os.path.exists("features"): + shutil.copytree("features", get_resource_path("features")) + return True + has_features = False + for f in os.listdir("."): + if not f.endswith(".feature"): + continue + if args["feature"] and args["feature"] != f: + continue + has_features = True + shutil.copyfile(f, os.path.join(get_resource_path("features"), os.path.basename(f))) + return has_features + + +""" +# clean logs to be able to run tests +# once again but on another building model and in another directory +# somehow does not work, thus test will be run in the same directory +# on each new run, directory will be deleted before each new run +# https://github.com/behave/behave/issues/871 +# run bimtester +# copy manually this code, run bimtester again, does not work on two directories +from behave.runner_util import reset_runtime +reset_runtime() + +""" + + +""" +from code_bimtester import run +myfeatures_path = "/home/hugo/.FreeCAD/Mod/bimtester/features_bimtester/fea_min/" +myifcfile_path = "/home/hugo/Documents/zeug_sort/z_some_ifc/" +ifcfilename = "example_model.ifc" +run.run_all(myfeatures_path, myifcfile_path, ifcfilename) + + +from code_bimtester import run +myfeatures_path = "/home/hugo/Documents/zeug_sort/ifcos_bimtester/myrun/" +run.run_all(myfeatures_path, myfeatures_path) + +""" + + +# TODO: if the ifc file name or path contains special character +# like German Umlaute behave gives an error + + +def run_intmp_tests(args={}): + + from behave import __version__ as behave_version + # https://github.com/behave/behave/issues/871 + if behave_version == "1.2.5": + print( + "At least behave version 1.2.6 is needed, but version {} found." + .format(behave_version) + ) + return False + + # mandatory parameter: ifcdir, featuredir + # optional parameter: ifcfilename + # copy features and steps to tmp, replace ifcdir in features files + # run + + # get ifcpath, this is the path the ifc file is in + if "ifcpath" in args and args["ifcpath"] != "": + # TODO check if path exists + ifc_path = args["ifcpath"] + else: + print("No ifc path was given.") + return False + + # get the features_path, the feature files where the tests are in + if "features" in args and args["features"] != "": + # TODO check if path exists, and if features dir is inside + features_path = os.path.join(args["features"], "features") + else: + print("No features path was given.") + return False + + if "ifcfilename" in args and args["ifcfilename"] != "": + # TODO check if file + ifc_filename = args["ifcfilename"] + else: + ifc_filename = None + + # set up paths + # a unique temp path should not be used + # behave raises an ambiguous step exception + # run_path = tempfile.mkdtemp() + # thus use the same path on every run + # but delete it if exists + run_path = os.path.join(tempfile.gettempdir(), "bimtesterfc") + if os.path.isdir(run_path): + from shutil import rmtree + rmtree(run_path) # fails on read only files + if os.path.isdir(run_path): + print("Delete former beimtester run dir {} failed".format(run_path)) + return False + os.mkdir(run_path) + report_path = os.path.join(run_path, "report") + copy_features_path = os.path.join(run_path, "features") + copy_steps_path = os.path.join(copy_features_path, "steps") + + # copy features files + # print(features_path) + # print(copy_features_path) + if os.path.exists(features_path): + shutil.copytree(features_path, copy_features_path) + + # replace ifcpath in feature files + # IMHO better than copy the ifc file which could be 500 MB + feature_files = os.listdir(copy_features_path) + # print(feature_files) + for feature_file in feature_files: + feature_file = os.path.join(copy_features_path, feature_file) + # print(feature_file) + # search the line + ff = open(feature_file, "r") + lines = ff.readlines() + ff.close() + theline = "" + for line in lines: + if "* The IFC file" in line and "must be provided" in line: + theline = line + if ifc_filename is None: + ifc_filename = os.path.basename(theline.split('"')[1]) + newifcline = ( + ' * The IFC file "{}" must be provided\n' + .format(os.path.join(ifc_path, ifc_filename)) + ) + # print(newifcline) + break + else: + print("The line which sets the ifc file to test was not found.") + newifcline = "" + # replace the line + if newifcline != "": + # https://stackoverflow.com/a/290494 + for line in fileinput.input(feature_file, inplace=True): + # the print replaces the line in the file + print(line.replace(theline, newifcline), end="") + + # copy step files and environment file + steps_path = os.path.join( + bimtester_path, + "features", + "steps" + ) + # print(steps_path) + # print(copy_steps_path) + if os.path.exists(steps_path): + shutil.copytree(steps_path, copy_steps_path) + + environment_file = os.path.join( + bimtester_path, + "features", + "environment.py" + ) + if os.path.isfile(environment_file): + shutil.copyfile( + environment_file, + os.path.join(copy_features_path, "environment.py") + ) + + # get advanced args + # print to console from inside step files, add "--no-capture" flag + # https://github.com/behave/behave/issues/346 + behave_args = [copy_features_path] + if "advanced_arguments" in args: + behave_args.extend(args["advanced_arguments"].split()) + elif "console" not in args: + behave_args.extend([ + "--no-capture", + "--format", + "json.pretty", + "--outfile", + os.path.join(report_path, "report.json") + ]) + print(behave_args) + + # run tests + from behave.__main__ import main as behave_main + behave_main(behave_args) + print("All tests are finished.") + + # delete steps + # shutil.rmtree(steps_path) + + return run_path + + +def run_all(the_features_path, the_ifcfile_path, the_ifcfile_name=None): + + # feature files + feature_files = os.listdir( + os.path.join(the_features_path, "features") + ) + # print(feature_files) + + # run bimtester + if the_ifcfile_name is None: + runpath = run_intmp_tests({ + "features": the_features_path, + "ifcpath": the_ifcfile_path + }) + else: + runpath = run_intmp_tests({ + "features": the_features_path, + "ifcpath": the_ifcfile_path, + "ifcfilename": the_ifcfile_name + }) + + # create html report + from .reports import generate_report + generate_report(runpath) + # print(runpath) + + # open the webbrowser + for ff in feature_files: + webbrowser.open(os.path.join( + runpath, + "report", + ff + ".html" + )) + + return True