BIMTester now operates on single feature files, which heavily improves usability

This commit is contained in:
Dion Moult
2021-02-02 12:34:14 +11:00
parent 1f4df32d04
commit ac95fb1aa2
20 changed files with 401 additions and 978 deletions
+110 -17
View File
@@ -1,29 +1,122 @@
# BIMTester # BIMTester
### Packages to be installed
+ behave
+ pystache
+ ifcopenshell
BIMTester lets you specify a set of exchange requirements, and automatically check whether or not BIM data, typically an
IFC file, complies with the requirements. You can run it automatically from a server, integrate it to your own
application, or use a GUI. It can generate reports in various formats including:
* HTML
* JSON
* XUnit
* BCF
* Zoom Smart View
Your exchange requirements are written in plain language which you can use to communicate to project teams and include
in contracts. Multiple languages are supported. A series of exchange requirement templates are provided to get started,
but you can, and are encouraged to, write your own tailored to your project. An example of a requirement looks like
this:
### Start bimtester Gui from a shell
``` ```
python3 ./bimtester.py -g Feature: Project setup
In order to view the BIM data
As any interested stakeholder
We need an IFC file
Scenario: Receiving a file
* IFC data must use the IFC4 schema
``` ```
### Create a binary out of the Python package Languages supported include (in alphabetical order):
+ The commands needs to be updated
+ 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`
* Dutch
* English
* French
* German
* Italian
If you are not a developer, we highly recommend simply installing an integrated version of BIMTester.
* If you use Blender, install the [BlenderBIM Add-on](https://blenderbim.org)
* If you use FreeCAD, install the [FreeCAD BIMTester Workbench](https://github.com/bimtester/bimtesterfc)
If you are a developer, read on!
## Installation
The following packages are required for BIMTester to function:
* behave
* pystache
* ifcopenshell
* PySide2 (optional: needed for GUI)
The repository does not contain translation files. You can generate them as shown.
### Translation files
+ the binary mo translation files are not part of the repo
+ they might be needed to be recreated
+ use the following commands on Linux
``` ```
cd YourIfcopenshellRepository/src/ifcbimtester/bimtester/locale cd IfcOpenShell/src/ifcbimtester/bimtester/locale
pybabel compile -d . pybabel compile -d .
```
## CLI Usage
BIMTester has a command line application. Check it out:
``` ```
$ python cli.py -h
usage: cli.py [-h] [-a ACTION] [--advanced-arguments ADVANCED_ARGUMENTS] [-c]
-f FEATURE -i IFC [-p PATH] [-r REPORT] [--lang LANG]
Runs unit tests for BIM data
optional arguments:
-h, --help show this help message and exit
-a ACTION, --action ACTION
Action to perform, from run/purge
--advanced-arguments ADVANCED_ARGUMENTS
Specify arguments to Behave
-c, --console Show results in the console
-f FEATURE, --feature FEATURE
Specify a feature file to test
-i IFC, --ifc IFC Specify a ifc file
-p PATH, --path PATH Define a path for use in tests
-r REPORT, --report REPORT
Specify an output file for a HTML report
--lang LANG Specify a language
```
You can turn it into a regular command by symlinking it to your bin folder.
```
$ ln -s /path/to/IfcOpenShell/src/ifcbimtester/cli.py /usr/local/bin/bimtester
$ bimtester -h
```
To run a test, we need an IFC to check and a feature file filled with requirements. The feature file is plaintext. Feel
free to copy the minimal example above.
```
# To see output in the console
$ bimtester -i test.ifc -f test.feature -c
# Or, if you want to generate a HTML report
$ bimtester -i test.ifc -f test.feature -r report.html
```
```
python ./gui.py
```
## Create a binary out of the Python package
TODO: Check if this works
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
```
@@ -1,3 +0,0 @@
# dummy file to be able to run the command to get a list of all steps
# the command has to be run in the parent directory of this one
# behave --steps-catalog
@@ -1,28 +1,28 @@
import os import os
from behave.model import Scenario from behave.model import Scenario
from logfile import create_logfile from logfile import create_logfile
from logfile import append_logfile from logfile import append_logfile
from zoom_smart_view import append_zoom_smartview from zoom_smart_view import append_zoom_smartview
from zoom_smart_view import create_zoom_smartview from zoom_smart_view import create_zoom_smartview
from bimtester.ifc import IfcStore
from bimtester.lang import switch_locale
this_path = os.path.dirname(os.path.realpath(__file__)) this_path = os.path.dirname(os.path.realpath(__file__))
def before_all(context): def before_all(context):
# get from userdata
userdata = context.config.userdata userdata = context.config.userdata
context.localedir = userdata.get("localedir")
context.ifcfile = userdata["ifcfile"] if context.config.lang:
context.ifcbasename = os.path.basename( switch_locale(userdata.get("localedir"), context.config.lang)
os.path.splitext(context.ifcfile)[0]
context.ifc_path = userdata["ifc"]
context.ifc_basename = os.path.basename(
os.path.splitext(context.ifc_path)[0]
) )
# do not break after a failed scenario
# https://community.osarch.org/discussion/comment/3328/#Comment_3328
continue_after_failed = userdata.getbool( continue_after_failed = userdata.getbool(
"runner.continue_after_failed_step", True "runner.continue_after_failed_step", True
) )
@@ -37,21 +37,21 @@ def before_all(context):
# set up log file # set up log file
context.thelogfile = os.path.join( context.thelogfile = os.path.join(
context.outpath, context.outpath,
context.ifcbasename + ".log" context.ifc_basename + ".log"
) )
create_logfile( create_logfile(
context.thelogfile, context.thelogfile,
context.ifcbasename, context.ifc_basename,
) )
# set up smart view file # set up smart view file
context.smview_file = os.path.join( context.smview_file = os.path.join(
context.outpath, context.outpath,
context.ifcbasename + ".bcsv" context.ifc_basename + ".bcsv"
) )
create_zoom_smartview( create_zoom_smartview(
context.smview_file, context.smview_file,
context.ifcbasename, context.ifc_basename,
) )
@@ -1,20 +1,16 @@
import gettext # noqa import gettext
from behave import given from behave import given
from behave import step from behave import step
import ifcdata_methods as idm
from utils import IfcFile from utils import IfcFile
from utils import switch_locale from bimtester.ifc import IfcStore
from bimtester.lang import _
the_lang = "en"
@step('The IFC schema "{schema}" must be provided') @step('The IFC schema "{schema}" must be provided')
def step_impl(context, schema): def step_impl(context, schema):
try: try:
if context.config.userdata.get('path'): if context.config.userdata.get("path"):
schema = os.path.join(context.config.userdata.get('path'), schema) schema = os.path.join(context.config.userdata.get("path"), schema)
IfcFile.load_schema(schema) IfcFile.load_schema(schema)
except: except:
assert False, f"The schema {schema} could not be loaded" assert False, f"The schema {schema} could not be loaded"
@@ -28,24 +24,10 @@ def step_impl(context, file):
assert False, f"The file {file} could not be loaded" assert False, f"The file {file} could not be loaded"
@given("The IFC file has been provided through an argument")
def step_impl(context):
switch_locale(context.localedir, the_lang)
idm.provide_ifcfile_by_argument(context)
@given('A file path has been provided through an argument')
def step_impl(context):
try:
assert context.config.userdata.get("path")
except:
assert False, f"The path {context.config.userdata.get('path')} could not be loaded"
@step("IFC data must use the {schema} schema") @step("IFC data must use the {schema} schema")
def step_impl(context, schema): def step_impl(context, schema):
switch_locale(context.localedir, the_lang) real_schema = IfcStore.file.schema
idm.has_ifcdata_specific_schema(context, schema) assert real_schema == schema, _("We expected a schema of {} but instead got {}").format(schema, real_schema)
@step('The IFC file "{file}" is exempt from being provided') @step('The IFC file "{file}" is exempt from being provided')
@@ -61,41 +43,43 @@ def step_impl(context, reason):
@step("The IFC file must be exported by application full name {fullname}") @step("The IFC file must be exported by application full name {fullname}")
def step_impl(context, fullname): def step_impl(context, fullname):
real_fullname = IfcFile.get().by_type("IfcApplication")[0].ApplicationFullName real_fullname = IfcStore.file.by_type("IfcApplication")[0].ApplicationFullName
assert real_fullname == fullname , ( assert real_fullname == fullname, (
"The IFC file was not exported by application full name {} " "The IFC file was not exported by application full name {} "
"instead it was exported by application full name {}" "instead it was exported by application full name {}".format(fullname, real_fullname)
.format(fullname, real_fullname)
) )
@step("The IFC file must be exported by application identifier {identifier}") @step("The IFC file must be exported by application identifier {identifier}")
def step_impl(context, identifier): def step_impl(context, identifier):
real_identifier = IfcFile.get().by_type("IfcApplication")[0].ApplicationIdentifier real_identifier = IfcStore.file.by_type("IfcApplication")[0].ApplicationIdentifier
assert real_identifier == identifier , ( assert (
"The IFC file was not exported by application identifier {} " real_identifier == identifier
"instead it was exported by identifier {}" ), "The IFC file was not exported by application identifier {} " "instead it was exported by identifier {}".format(
.format(identifier, real_identifier) identifier, real_identifier
) )
@step("The IFC file must be exported by the application version {version}") @step("The IFC file must be exported by the application version {version}")
def step_impl(context, version): def step_impl(context, version):
real_version = IfcFile.get().by_type("IfcApplication")[0].Version real_version = IfcStore.file.by_type("IfcApplication")[0].Version
assert real_version == version , ( assert (
"The IFC file was not exported by application version {} " real_version == version
"instead it was exported by version {}" ), "The IFC file was not exported by application version {} " "instead it was exported by version {}".format(
.format(version, real_version) version, real_version
) )
@step("IFC data header must have a file description of {header_file_description} such as the new Allplan IFC exporter creates it") @step(
"IFC data header must have a file description of {header_file_description} such as the new Allplan IFC exporter creates it"
)
def step_impl(context, header_file_description): def step_impl(context, header_file_description):
is_header_file_description = IfcFile.get().wrapped_data.header.file_description.description is_header_file_description = IfcStore.file.wrapped_data.header.file_description.description
assert str(is_header_file_description) == header_file_description , ( assert (
"The file was not exported by the new ifc exporter in Allplan. File description header: {}" str(is_header_file_description) == header_file_description
.format(is_header_file_description) ), "The file was not exported by the new ifc exporter in Allplan. File description header: {}".format(
is_header_file_description
) )
@@ -1,19 +1,6 @@
from behave import step from behave import step
import ifcdata_methods as idm
from utils import switch_locale
the_lang = "de"
@given("Die IFC-Datei wurde durch einen Startparameter zur Verfügung gestellt")
def step_impl(context):
switch_locale(context.localedir, the_lang)
idm.provide_ifcfile_by_argument(context)
@step("Die IFC-Daten müssen das {schema} Schema benutzen") @step("Die IFC-Daten müssen das {schema} Schema benutzen")
def step_impl(context, schema): def step_impl(context, schema):
switch_locale(context.localedir, the_lang) context.execute_steps(f"* IFC data must use the {schema} schema")
idm.has_ifcdata_specific_schema(context, schema)
@@ -1,22 +1,6 @@
from behave import step from behave import step
import ifcdata_methods as idm
from utils import switch_locale
the_lang = "fr"
"""
# TODO the next line needs translation
@given("The IFC file has been provided through an argument")
def step_impl(context):
switch_locale(context.localedir, the_lang)
idm.provide_ifcfile_by_argument(context)
"""
@step("Les données IFC doivent utiliser le schéma {schema}") @step("Les données IFC doivent utiliser le schéma {schema}")
def step_impl(context, schema): def step_impl(context, schema):
switch_locale(context.localedir, the_lang) context.execute_steps(f"* IFC data must use the {schema} schema")
idm.has_ifcdata_specific_schema(context, schema)
@@ -1,19 +1,6 @@
from behave import step from behave import step
import ifcdata_methods as idm
from utils import switch_locale
the_lang = "it"
@given("Il file IFC è stato fornito attraverso un argumento")
def step_impl(context):
switch_locale(context.localedir, the_lang)
idm.provide_ifcfile_by_argument(context)
@step("I dati IFC devono seguire lo schema {schema}") @step("I dati IFC devono seguire lo schema {schema}")
def step_impl(context, schema): def step_impl(context, schema):
switch_locale(context.localedir, the_lang) context.execute_steps(f"* IFC data must use the {schema} schema")
idm.has_ifcdata_specific_schema(context, schema)
@@ -1,19 +0,0 @@
from utils import IfcFile
def provide_ifcfile_by_argument(context):
try:
IfcFile.load(context.config.userdata.get("ifcfile"))
except:
assert False, (
_("The IFC {} file could not be loaded")
.format(context.config.userdata.get('ifcfile'))
)
def has_ifcdata_specific_schema(context, target_schema):
real_schema = IfcFile.get().schema
assert real_schema == target_schema, (
_("We expected a schema of {} but instead got {}")
.format(target_schema, real_schema)
)
@@ -1,22 +1,6 @@
from behave import step from behave import step
import ifcdata_methods as idm
from utils import switch_locale
the_lang = "nl"
"""
# TODO the next line needs translation
@given("The IFC file has been provided through an argument")
def step_impl(context):
switch_locale(context.localedir, the_lang)
idm.provide_ifcfile_by_argument(context)
"""
@step("IFC-gegevens moeten het {schema} -schema gebruiken") @step("IFC-gegevens moeten het {schema} -schema gebruiken")
def step_impl(context, schema): def step_impl(context, schema):
switch_locale(context.localedir, the_lang) context.execute_steps(f"* IFC data must use the {schema} schema")
idm.has_ifcdata_specific_schema(context, schema)
@@ -2,36 +2,37 @@ from behave import step
from utils import assert_attribute from utils import assert_attribute
from utils import IfcFile from utils import IfcFile
from bimtester.ifc import IfcStore
@step("The project must have an identifier of {guid}") @step("The project must have an identifier of {guid}")
def step_impl(context, guid): def step_impl(context, guid):
assert_attribute(IfcFile.get().by_type("IfcProject")[0], "GlobalId", guid) assert_attribute(IfcStore.file.by_type("IfcProject")[0], "GlobalId", guid)
@step('The project name, code, or short identifier must be "{value}"') @step('The project name, code, or short identifier must be "{value}"')
def step_impl(context, value): def step_impl(context, value):
assert_attribute(IfcFile.get().by_type("IfcProject")[0], "Name", value) assert_attribute(IfcStore.file.by_type("IfcProject")[0], "Name", value)
@step('The project must have a longer form name of "{value}"') @step('The project must have a longer form name of "{value}"')
def step_impl(context, value): def step_impl(context, value):
assert_attribute(IfcFile.get().by_type("IfcProject")[0], "LongName", value) assert_attribute(IfcStore.file.by_type("IfcProject")[0], "LongName", value)
@step('The project must be described as "{value}"') @step('The project must be described as "{value}"')
def step_impl(context, value): def step_impl(context, value):
assert_attribute(IfcFile.get().by_type("IfcProject")[0], "Description", value) assert_attribute(IfcStore.file.by_type("IfcProject")[0], "Description", value)
@step('The project must be categorised under "{value}"') @step('The project must be categorised under "{value}"')
def step_impl(context, value): def step_impl(context, value):
assert_attribute(IfcFile.get().by_type("IfcProject")[0], "ObjectType", value) assert_attribute(IfcStore.file.by_type("IfcProject")[0], "ObjectType", value)
@step('The project must contain information about the "{value}" phase') @step('The project must contain information about the "{value}" phase')
def step_impl(context, value): def step_impl(context, value):
assert_attribute(IfcFile.get().by_type("IfcProject")[0], "Phase", value) assert_attribute(IfcStore.file.by_type("IfcProject")[0], "Phase", value)
@step("The project must contain 3D geometry representing the shape of objects") @step("The project must contain 3D geometry representing the shape of objects")
@@ -4,7 +4,6 @@ import ifcopenshell.express
import ifcopenshell.util import ifcopenshell.util
import ifcopenshell.util.element import ifcopenshell.util.element
class IfcFile(object): class IfcFile(object):
file = None file = None
bookmarks = {} bookmarks = {}
@@ -108,60 +107,36 @@ def assert_elements(
message_all_falseelems, message_all_falseelems,
message_some_falseelems, message_some_falseelems,
message_no_elems, message_no_elems,
parameter=None parameter=None,
): ):
if elemcount > 0 and falsecount == 0: if elemcount > 0 and falsecount == 0:
return # Test OK return # Test OK
elif elemcount == 0: elif elemcount == 0:
assert False, ( assert False, message_no_elems.format(ifc_class=ifc_class)
message_no_elems.format(
ifc_class=ifc_class
)
)
elif falsecount == elemcount: elif falsecount == elemcount:
if parameter is None: if parameter is None:
assert False, ( assert False, message_all_falseelems.format(elemcount=elemcount, ifc_class=ifc_class)
message_all_falseelems.format(
elemcount=elemcount,
ifc_class=ifc_class
)
)
else: else:
assert False, ( assert False, message_all_falseelems.format(elemcount=elemcount, ifc_class=ifc_class, parameter=parameter)
message_all_falseelems.format(
elemcount=elemcount,
ifc_class=ifc_class,
parameter=parameter
)
)
elif falsecount > 0 and falsecount < elemcount: elif falsecount > 0 and falsecount < elemcount:
if parameter is None: if parameter is None:
assert False, ( assert False, message_some_falseelems.format(
message_some_falseelems.format( falsecount=falsecount,
falsecount=falsecount, elemcount=elemcount,
elemcount=elemcount, ifc_class=ifc_class,
ifc_class=ifc_class, falseelems=falseelems,
falseelems=falseelems,
)
) )
else: else:
assert False, ( assert False, message_some_falseelems.format(
message_some_falseelems.format( falsecount=falsecount,
falsecount=falsecount, elemcount=elemcount,
elemcount=elemcount, ifc_class=ifc_class,
ifc_class=ifc_class, falseelems=falseelems,
falseelems=falseelems, parameter=parameter,
parameter=parameter
)
) )
else: else:
assert False, _("Error in falsecount, something went wrong.") assert False, _("Error in falsecount, something went wrong.")
def switch_locale(locale_dir, locale_id="en"): def switch_locale(locale_dir, locale_id="en"):
newlang = gettext.translation( pass
"messages",
localedir=locale_dir,
languages=[locale_id]
)
newlang.install()
+23 -138
View File
@@ -1,71 +1,33 @@
# TODO: all args should be passed to the gui,
# either pass them further to behave or make it possible to edit them before
# TODO: if browse widgets will be canceled, last QLineEdit should be restored
# TODO: keep path or file if in browse widget canceled
# TODO: make a frame around features direcory chooser and
# use features path from ifc button
import os import os
import sys
import bimtester.run
from PySide2 import QtCore from PySide2 import QtCore
from PySide2 import QtGui from PySide2 import QtGui
from PySide2 import QtWidgets from PySide2 import QtWidgets
from .run import run_all
def run():
app = QtWidgets.QApplication(sys.argv)
form = GuiWidgetBimTester()
form.show()
sys.exit(app.exec_())
class GuiWidgetBimTester(QtWidgets.QWidget): class GuiWidgetBimTester(QtWidgets.QWidget):
def __init__(self, args=[]):
def __init__(
self,
featurespath="",
ifcfile="",
get_featurepath_from_ifcpath=False,
args=[]
):
super(GuiWidgetBimTester, self).__init__() super(GuiWidgetBimTester, self).__init__()
user_path = os.path.expanduser("~")
# get features dir
# print(featurespath)
if not os.path.isdir(featurespath):
featurespath = user_path
# get ifc file
# print(ifcfile)
if not os.path.isfile(ifcfile):
ifcfile = user_path
self.initial_featurespath = featurespath
self.initial_ifcfile = ifcfile
self.get_featurepath_from_ifcpath = get_featurepath_from_ifcpath
self.args = args self.args = args
# print(self.initial_featurespath)
# print(self.initial_ifcfile)
# print(self.get_featurepath_from_ifcpath)
# init ui
self._setup_ui() self._setup_ui()
def __del__(self,): # http://forum.freecadweb.org/viewtopic.php?f=18&t=10732&start=10#p86493
# need as fix for qt event error def __del__(self):
# http://forum.freecadweb.org/viewtopic.php?f=18&t=10732&start=10#p86493
return return
def _setup_ui(self): 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__)) package_path = os.path.dirname(os.path.realpath(__file__))
iconpath = os.path.join( iconpath = os.path.join(package_path, "resources", "icons", "bimtester.ico")
package_path, "resources", "icons", "bimtester.ico"
)
""" """
# as svg # as svg
# https://stackoverflow.com/a/35138314 # https://stackoverflow.com/a/35138314
@@ -78,6 +40,7 @@ class GuiWidgetBimTester(QtWidgets.QWidget):
#) #)
#theicon.sizeHint() #theicon.sizeHint()
""" """
# as pixmap # as pixmap
theicon = QtWidgets.QLabel(self) theicon = QtWidgets.QLabel(self)
iconpixmap = QtGui.QPixmap(iconpath) iconpixmap = QtGui.QPixmap(iconpath)
@@ -87,7 +50,6 @@ class GuiWidgetBimTester(QtWidgets.QWidget):
# ifc file # ifc file
_ifcfile_label = QtWidgets.QLabel("IFC file", self) _ifcfile_label = QtWidgets.QLabel("IFC file", self)
self.ifcfile_text = QtWidgets.QLineEdit() self.ifcfile_text = QtWidgets.QLineEdit()
self.set_ifcfile(self.initial_ifcfile)
_ifcfile_browse_btn = QtWidgets.QToolButton() _ifcfile_browse_btn = QtWidgets.QToolButton()
_ifcfile_browse_btn.setText("...") _ifcfile_browse_btn.setText("...")
_ifcfile_browse_btn.clicked.connect(self.select_ifcfile) _ifcfile_browse_btn.clicked.connect(self.select_ifcfile)
@@ -95,36 +57,20 @@ class GuiWidgetBimTester(QtWidgets.QWidget):
# feature files path # feature files path
# use a layout with a frame and a title, see solver framework tp # use a layout with a frame and a title, see solver framework tp
# beside button # beside button
ffifc_str = ( ffifc_str = "Feature files in a directory 'features' beside the IFC file."
"Feature files in a directory 'features' beside the IFC file."
)
featuredirfromifc_label = QtWidgets.QLabel(ffifc_str, self) 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 # path browser and line edit
_ffdir_str = ( _ffdir_str = "Feature files directory. " "'features' directory has to be in there."
"Feature files directory. "
"'features' directory has to be in there."
)
_featurefilesdir_label = QtWidgets.QLabel(_ffdir_str, self) _featurefilesdir_label = QtWidgets.QLabel(_ffdir_str, self)
self.featurefilesdir_text = QtWidgets.QLineEdit() self.featurefilesdir_text = QtWidgets.QLineEdit()
self.set_featurefilesdir(self.initial_featurespath)
self.feafilesdir_browse_btn = QtWidgets.QToolButton() self.feafilesdir_browse_btn = QtWidgets.QToolButton()
self.feafilesdir_browse_btn.setText("...") self.feafilesdir_browse_btn.setText("...")
self.feafilesdir_browse_btn.clicked.connect( self.feafilesdir_browse_btn.clicked.connect(self.select_featurefilesdir)
self.select_featurefilesdir
)
# buttons # buttons
self.run_button = QtWidgets.QPushButton( self.run_button = QtWidgets.QPushButton(QtGui.QIcon.fromTheme("document-new"), "Run")
QtGui.QIcon.fromTheme("document-new"), "Run" self.close_button = QtWidgets.QPushButton(QtGui.QIcon.fromTheme("window-close"), "Close")
)
self.close_button = QtWidgets.QPushButton(
QtGui.QIcon.fromTheme("window-close"), "Close"
)
self.run_button.clicked.connect(self.run_bimtester) self.run_button.clicked.connect(self.run_bimtester)
self.close_button.clicked.connect(self.close_widget) self.close_button.clicked.connect(self.close_widget)
_buttons = QtWidgets.QHBoxLayout() _buttons = QtWidgets.QHBoxLayout()
@@ -136,7 +82,6 @@ class GuiWidgetBimTester(QtWidgets.QWidget):
layout.addWidget(theicon, 1, 0, alignment=QtCore.Qt.AlignRight) layout.addWidget(theicon, 1, 0, alignment=QtCore.Qt.AlignRight)
layout.addWidget(featuredirfromifc_label, 2, 0) layout.addWidget(featuredirfromifc_label, 2, 0)
layout.addWidget(self.featuredirfromifc_cb, 2, 1)
layout.addWidget(_featurefilesdir_label, 3, 0) layout.addWidget(_featurefilesdir_label, 3, 0)
layout.addWidget(self.featurefilesdir_text, 4, 0) layout.addWidget(self.featurefilesdir_text, 4, 0)
@@ -153,17 +98,8 @@ class GuiWidgetBimTester(QtWidgets.QWidget):
layout.setRowStretch(0, 10) layout.setRowStretch(0, 10)
self.setLayout(layout) self.setLayout(layout)
# set button state, needs to be here after gui has beed set up
self.featuredirfromifc_cb.setChecked(self.get_featurepath_from_ifcpath)
# **********************************************************
def select_ifcfile(self): def select_ifcfile(self):
# print(self.get_ifcfile()) ifcfile = QtWidgets.QFileDialog.getOpenFileName(self, dir=self.get_ifcfile())[0]
# print(os.path.isfile(self.get_ifcfile()))
ifcfile = QtWidgets.QFileDialog.getOpenFileName(
self,
dir=self.get_ifcfile()
)[0]
self.set_ifcfile(ifcfile) self.set_ifcfile(ifcfile)
def set_ifcfile(self, a_file): def set_ifcfile(self, a_file):
@@ -172,26 +108,13 @@ class GuiWidgetBimTester(QtWidgets.QWidget):
def get_ifcfile(self): def get_ifcfile(self):
return self.ifcfile_text.text() return self.ifcfile_text.text()
def featuredirfromifc_clicked(self):
if self.featuredirfromifc_cb.isChecked() is True:
self.set_featurefilesdir("")
self.featurefilesdir_text.setEnabled(False)
self.feafilesdir_browse_btn.setEnabled(False)
else:
self.set_featurefilesdir(self.initial_featurespath)
self.featurefilesdir_text.setEnabled(True)
self.feafilesdir_browse_btn.setEnabled(True)
def select_featurefilesdir(self): def select_featurefilesdir(self):
thedir = self.featurefilesdir_text.text() 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( features_path = QtWidgets.QFileDialog.getExistingDirectory(
self, self,
caption="Choose features directory ...", caption="Choose features directory ...",
dir=thedir, dir=thedir,
options=QtWidgets.QFileDialog.HideNameFilterDetails options=QtWidgets.QFileDialog.HideNameFilterDetails,
) )
self.set_featurefilesdir(features_path) self.set_featurefilesdir(features_path)
@@ -201,46 +124,11 @@ class GuiWidgetBimTester(QtWidgets.QWidget):
def get_featurefilesdir(self): def get_featurefilesdir(self):
return self.featurefilesdir_text.text() return self.featurefilesdir_text.text()
# **********************************************************
def run_bimtester(self): def run_bimtester(self):
print("Run BIMTester by the GUI") print("Run BIMTester by the GUI")
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor)
# get features dir the_features_path = self.get_featurefilesdir()
if self.featuredirfromifc_cb.isChecked() is True:
ifcfile = self.get_ifcfile()
print(
"Make sure the feature files are beside "
"the ifc file in a directory named 'features'."
)
if ifcfile == "":
# os.path.realpath("") would return the cmd dir and not ""
print(
"No ifcfile given, "
"thus features files path will be set to ''."
)
the_features_path = ""
elif os.path.isfile(ifcfile) is not True:
# if ifcfile does not exist set features path to ""
print(
"The ifcfile does not exist, "
"thus features files path will be set to ''."
)
the_features_path = ""
else:
ifcfilepath = os.path.dirname(os.path.realpath(ifcfile))
if os.path.isdir(ifcfilepath):
the_features_path = ifcfilepath
else:
print(
"ifcfilepath does not exist, "
"thus features files path will be set to ''."
"this shold never happen, please debug. "
)
the_features_path = ""
else:
the_features_path = self.get_featurefilesdir()
print(the_features_path) print(the_features_path)
# get ifc file # get ifc file
@@ -252,14 +140,11 @@ class GuiWidgetBimTester(QtWidgets.QWidget):
patched_args["featuresdir"] = the_features_path patched_args["featuresdir"] = the_features_path
patched_args["ifcfile"] = the_ifcfile patched_args["ifcfile"] = the_ifcfile
# run bimtester bimtester.run.TestRunner("file.ifc").run({})
status = run_all(patched_args)
print(status)
QtWidgets.QApplication.restoreOverrideCursor() QtWidgets.QApplication.restoreOverrideCursor()
def close_widget(self): def close_widget(self):
print("Close BIMTester Gui")
self.close() self.close()
def closeEvent(self, ev): def closeEvent(self, ev):
+3
View File
@@ -0,0 +1,3 @@
class IfcStore:
path = ""
file = None
+15
View File
@@ -0,0 +1,15 @@
import gettext
translation = None
def _(message):
if translation:
return translation(message)
return message
def switch_locale(locale_dir, locale_id="en"):
global translation
newlang = gettext.translation("messages", localedir=locale_dir, languages=[locale_id])
newlang.install()
translation = newlang.gettext
+96 -135
View File
@@ -1,58 +1,26 @@
import datetime import datetime
import gettext # noqa
import json import json
import os import os
import pystache import pystache
from bimtester.lang import _
from .features.steps.utils import switch_locale
def generate_report( class ReportGenerator:
report_dir=".", def __init__(self):
use_report_folder=True, try:
report_file_name="report.json", # PyInstaller creates a temp folder and stores path in _MEIPASS
html_template_file_path="", self.base_path = sys._MEIPASS
report_file="" except Exception:
): self.base_path = os.path.dirname(os.path.realpath(__file__))
# TODO use far less parameter def generate(self, report_json, output_file):
# to be discussed with other devs print("# Generating HTML reports.")
print("# Generating HTML reports now.") report = json.loads(open(report_json).read())
for feature in report:
self.generate_feature_report(feature, output_file)
# get locale path def generate_feature_report(self, feature, output_file):
localedir = os.path.join(
os.path.dirname(os.path.realpath(__file__)),
"locale"
)
# get html template path
report_template_path = os.path.join(
os.path.dirname(os.path.realpath(__file__)),
"resources",
"reports"
)
if html_template_file_path:
report_template_path = html_template_file_path
# get report file and report dir
if report_file:
report_file = report_file
report_dir = os.path.dirname(report_file)
else:
if use_report_folder:
report_dir = os.path.join(report_dir, "report")
report_file = os.path.join(report_dir, report_file_name)
# print(report_file)
if not os.path.isdir(report_dir):
return print("Report directory does not exist.")
if not os.path.isfile(report_file):
return print("Report file does not exist.")
# read json report and create html report for each feature
report = json.loads(open(report_file).read())
for feature in report:
file_name = os.path.basename(feature["location"]).split(":")[0] file_name = os.path.basename(feature["location"]).split(":")[0]
data = { data = {
"file_name": file_name, "file_name": file_name,
@@ -62,108 +30,101 @@ def generate_report(
"is_success": feature["status"] == "passed", "is_success": feature["status"] == "passed",
"scenarios": [], "scenarios": [],
} }
if "elements" not in feature: if "elements" not in feature:
if "status" in feature and feature["status"] == "skipped": if "status" in feature and feature["status"] == "skipped":
print("Feature was skipped. No html report will be created.") print("Feature was skipped. No html report will be created.")
else: else:
print("For a unknown reason no html report well be created.") print("For a unknown reason no html report well be created.")
# happens if the feature file does not consist of any valid Scenario # happens if the feature file does not consist of any valid Scenario
continue return
for scenario in feature["elements"]: for scenario in feature["elements"]:
steps = [] scenario_data = self.process_scenario(scenario)
total_duration = 0 if scenario_data:
if len(scenario["steps"]) == 0: data["scenarios"].append(scenario_data)
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"], "<b>" + a["value"] + "</b>")
if "result" not in step:
step["result"] = {}
step["result"]["status"] = "skipped"
step["result"]["duration"] = 0
step["result"]["error_message"] = "This requirement has been skipped due to a previous failing step."
elif 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": step["result"]["status"] == "undefined",
"is_skipped": step["result"]["status"] == "skipped",
"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_passes"] = sum([s["total_passes"] for s in data["scenarios"]])
data["total_steps"] = sum([s["total_steps"] 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) data["pass_rate"] = round((data["total_passes"] / data["total_steps"]) * 100)
# translate report data.update(self.get_template_strings())
# json.dump(mydict, myfile, indent=4)
# workaround for retrieving the feature file language
print(feature["keyword"])
if feature["keyword"] == "Feature":
switch_locale(localedir, "en")
elif feature["keyword"] == "Funktionalität":
switch_locale(localedir, "de")
elif feature["keyword"] == "Fonctionnalité":
switch_locale(localedir, "fr")
elif feature["keyword"] == "Funzionalità":
switch_locale(localedir, "it")
elif feature["keyword"] == "Functionaliteit":
switch_locale(localedir, "nl")
else:
# standard English
switch_locale(localedir, "en")
strings_report = get_html_template_strings()
# print(strings_report)
data.update(strings_report)
# print(data)
html_report = os.path.join(report_dir, "{}.html".format(file_name)) with open(output_file, "w", encoding="utf8") as out:
html_tmpl = os.path.join(report_template_path, "template.html") with open(
with open(html_report, "w", encoding="utf8") as out: os.path.join(self.base_path, "resources", "reports", "template.html"), encoding="utf8"
with open(html_tmpl, encoding="utf8") as template: ) as template:
out.write(pystache.render(template.read(), data)) out.write(pystache.render(template.read(), data))
def process_scenario(self, scenario):
if len(scenario["steps"]) == 0:
print("Scenario '{}' in feature '{}' has no steps.".format(scenario["name"], feature["name"]))
return
def get_html_template_strings(): steps = []
total_duration = 0
return { for step in scenario["steps"]:
"tr_lang": _("en"), step_data = self.process_step(step)
"tr_success": _("Success"), total_duration += step_data["time_raw"]
"tr_failure": _("Failure"), steps.append(step_data)
"tr_tests_passed": _("Tests passed"),
"tr_duration": _("Duration"), total_passes = len([s for s in steps if s["is_success"] is True])
"tr_auditing": _("OpenBIM auditing is a feature of"), total_steps = len(steps)
"tr_and": _("and") pass_rate = round((total_passes / total_steps) * 100)
}
return {
"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,
}
def process_step(self, step):
name = step["name"]
if "match" in step and "arguments" in step["match"]:
for a in step["match"]["arguments"]:
name = name.replace(a["value"], "<b>" + a["value"] + "</b>")
if "result" not in step:
step["result"] = {}
step["result"]["status"] = "skipped"
step["result"]["duration"] = 0
step["result"][
"error_message"
] = "This requirement has been skipped due to a previous failing step."
elif 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."
data = {
"name": name,
"time_raw": step["result"]["duration"],
"time": round(step["result"]["duration"], 2),
"is_success": step["result"]["status"] == "passed",
"is_unspecified": step["result"]["status"] == "undefined",
"is_skipped": step["result"]["status"] == "skipped",
"error_message": None
if step["result"]["status"] == "passed"
else step["result"]["error_message"],
}
# TODO: there is probably a better way of doing this
if isinstance(data["error_message"], list):
data["error_message"] = data["error_message"][1]
return data
def get_template_strings(self):
return {
"_lang": _("en"),
"_success": _("Success"),
"_failure": _("Failure"),
"_tests_passed": _("Tests passed"),
"_duration": _("Duration"),
"_auditing": _("OpenBIM auditing is a feature of"),
"_and": _("and"),
}
@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang={{tr_lang}}> <html lang={{_lang}}>
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -32,8 +32,8 @@
<h1>{{name}}</h1> <h1>{{name}}</h1>
<p><strong>{{time}} {{file_name}}</strong></p> <p><strong>{{time}} {{file_name}}</strong></p>
<hr> <hr>
<span class="{{#is_success}}success{{/is_success}}{{^is_success}}failure{{/is_success}}">{{#is_success}}{{tr_success}}{{/is_success}}{{^is_success}}{{tr_failure}}{{/is_success}}</span> <span class="{{#is_success}}success{{/is_success}}{{^is_success}}failure{{/is_success}}">{{#is_success}}{{_success}}{{/is_success}}{{^is_success}}{{_failure}}{{/is_success}}</span>
{{tr_tests_passed}}: <strong>{{total_passes}} / {{total_steps}}</strong> ({{pass_rate}}%) {{_tests_passed}}: <strong>{{total_passes}} / {{total_steps}}</strong> ({{pass_rate}}%)
<br /> <br />
<p class="description"> <p class="description">
{{#description}} {{#description}}
@@ -46,10 +46,10 @@
<section> <section>
<h2>{{name}}</h2> <h2>{{name}}</h2>
<p> <p>
<span class="{{#is_success}}success{{/is_success}}{{^is_success}}failure{{/is_success}}">{{#is_success}}{{tr_success}}{{/is_success}}{{^is_success}}{{tr_failure}}{{/is_success}}</span> <span class="{{#is_success}}success{{/is_success}}{{^is_success}}failure{{/is_success}}">{{#is_success}}{{_success}}{{/is_success}}{{^is_success}}{{_failure}}{{/is_success}}</span>
{{tr_tests_passed}}: <strong>{{total_passes}} / {{total_steps}}</strong> ({{pass_rate}}%) {{_tests_passed}}: <strong>{{total_passes}} / {{total_steps}}</strong> ({{pass_rate}}%)
<span class="time"> <span class="time">
{{tr_duration}}: {{time}}s {{_duration}}: {{time}}s
</span> </span>
</p> </p>
<ol> <ol>
@@ -72,7 +72,7 @@
<hr> <hr>
<footer> <footer>
<p> <p>
{{tr_auditing}} <a href="https://blenderbim.org/">BlenderBIM</a> {{tr_and}} <a href="http://ifcopenshell.org/">IfcOpenShell</a>. {{_auditing}} <a href="https://blenderbim.org/">BlenderBIM</a> {{_and}} <a href="http://ifcopenshell.org/">IfcOpenShell</a>.
</p> </p>
</footer> </footer>
</body> </body>
+41 -326
View File
@@ -1,343 +1,58 @@
import behave.formatter.pretty # Needed for pyinstaller to package it
import os import os
import shutil
import sys import sys
import shutil
import tempfile import tempfile
import webbrowser import ifcopenshell
import behave.formatter.pretty # Needed for pyinstaller to package it
from bimtester.ifc import IfcStore
from bimtester.lang import switch_locale
from behave import __version__ as behave_version
from behave.__main__ import main as behave_main
# TODO: if the ifc file name or path contains special character class TestRunner:
# like German Umlaute behave gives an error def __init__(self, ifc_path, ifc=None):
IfcStore.path = ifc_path
IfcStore.file = ifc if ifc else ifcopenshell.open(ifc_path)
try:
# PyInstaller creates a temp folder and stores path in _MEIPASS
self.base_path = sys._MEIPASS
except Exception:
self.base_path = os.path.dirname(os.path.realpath(__file__))
# get bimtester source code module path self.locale_path = os.path.join(self.base_path, "locale")
bimtester_path = os.path.dirname(os.path.realpath(__file__))
# print(bimtester_path)
locale_path = os.path.join(bimtester_path, "locale")
def run(self, args):
print("# Run tests.")
tmpdir = tempfile.mkdtemp()
features_path = os.path.join(tmpdir, "features")
report_json = os.path.join(tmpdir, "report.json")
shutil.copytree(os.path.join(self.base_path, "features"), features_path)
shutil.copy(args["feature"], features_path)
behave_main(self.get_behave_args(args, features_path, report_json))
print("# All tests are finished.")
return report_json
try: def get_behave_args(self, args, features_path, report_json):
# 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):
print("# Run tests.")
report_file = os.path.join("report", "report.json")
if "copyintemprun" in args and args["copyintemprun"] is True:
print("copyintemprun")
is_copyintemprun = True
args, copy_base_path = copy_intmp_tests(args)
features_path = os.path.join(copy_base_path, "features")
report_file = os.path.join(copy_base_path, report_file)
else:
print("No copyintemprun")
is_copyintemprun = False
if not get_features(args):
print("No features could be found to check.")
return False
features_path = get_resource_path("features")
# get behave args
behave_args = get_behave_args(args, features_path, report_file)
# run tests
if behave_args != []:
run_behave(behave_args)
else:
print("Error, not able to run behave because of empty behave args.")
return False
if is_copyintemprun is True:
return report_file
else:
return True
def get_behave_args(args, features_path, report_file):
if os.path.isdir(features_path):
behave_args = [features_path] behave_args = [features_path]
else:
return []
if os.path.isdir(locale_path): behave_args.extend(["--define", "localedir={}".format(self.locale_path)])
behave_args.extend([
# path for translation files
# next two lines are one arg
"--define",
"localedir={}".format(locale_path)
])
else:
print(
"Error, translation locals path '{}' does not exist."
.format(locale_path)
)
if args["advanced_arguments"]: if args["advanced_arguments"]:
behave_args.extend(args["advanced_arguments"].split()) behave_args.extend(args["advanced_arguments"].split())
if args["ifcfile"]: if args["ifc"]:
behave_args.extend([ behave_args.extend(["--define", "ifc={}".format(args["ifc"])])
# next two lines are one arg
"--define",
"ifcfile={}".format(args["ifcfile"])
])
if args["path"]: if args["path"]:
behave_args.extend([ behave_args.extend(["--define", "path={}".format(args["path"])])
# next two lines are one arg
"--define",
"path={}".format(args["path"])
])
if not args["console"]: if args["lang"]:
behave_args.extend([ behave_args.extend(["--lang={}".format(args["lang"])])
# redirect prints in step methods
# if step fails some output is catched, thus might not be printed if not args["console"]:
# https://github.com/behave/behave/issues/346 # https://github.com/behave/behave/issues/346
"--no-capture", behave_args.extend(["--no-capture", "--format", "json.pretty", "--outfile", report_json])
# next two lines are one arg
"--format",
"json.pretty",
# report file, if relative, than relative to current shell path
# next two lines are one arg
"--outfile",
report_file,
])
return behave_args return behave_args
def run_behave(behave_args):
from json import dumps
print(dumps(behave_args, indent=4))
from behave.__main__ import main as behave_main
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()
"""
def copy_intmp_tests(args={}):
print("# Copy features and steps to temp.")
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
# print(args)
# get the features_path, the dir where the feature files to test are in
if ("featuresdir" in args and args["featuresdir"] != ""):
is_features = True
the_features_path = os.path.join(args["featuresdir"], "features")
if not os.path.isdir(the_features_path):
print(
"Error, the features directory '{}' does not exist."
.format(the_features_path)
)
return False
else:
is_features = False
# get ifc path and ifc filename
if ("ifcfile" in args and args["ifcfile"] != ""):
is_ifcfile = True
ifcfile = args["ifcfile"]
if os.path.isfile(ifcfile) is not True:
print("Error, the ifc file '{}' does not exist.".format(ifcfile))
return False
ifc_path = os.path.dirname(os.path.realpath(ifcfile))
if os.path.isdir(ifc_path) is False:
print("ifc path does not exist.")
return False
else:
is_ifcfile = False
# print(is_features)
# print(is_ifcfile)
if is_features is True and is_ifcfile is True:
print("features given, ifcfile given.")
elif is_features is False and is_ifcfile is True:
print("features given, ifcfile NOT given.")
# features = ifcfile directory
the_features_path = os.path.join(ifc_path, "features")
elif is_features is True and is_ifcfile is False:
print("features given, ifcfile NOT given.")
# the ifcfile provided in the feature files is used
# TODO What will be passed as ifcfile arg?
print("Not yet implemented.")
return False
elif is_features is False and is_ifcfile is False:
print("features NOT given, ifcfile NOT given.")
# the current directory = features
# the ifcfile provided in the feature files is used
# TODO What will be passed as ifcfile arg?
print("Not yet implemented.")
return False
else:
print("Error: this should never happen, please debug.")
return False
# set up paths
# a unique temp path should not be used
# behave raises an ambiguous step exception
# copy_base_path = tempfile.mkdtemp()
# thus use the same path on every run
# but delete it if exists
copy_base_path = os.path.join(tempfile.gettempdir(), "bimtesterfc")
if os.path.isdir(copy_base_path):
from shutil import rmtree
rmtree(copy_base_path) # fails on read only files
if os.path.isdir(copy_base_path):
print(
"Delete former beimtester run dir '{}' failed"
.format(copy_base_path)
)
return False
os.mkdir(copy_base_path)
copy_features_path = os.path.join(copy_base_path, "features")
# copy features path from bimtester source code
srccode_features_path = os.path.join(
bimtester_path,
"features",
)
# print(srccode_features_path)
if os.path.exists(srccode_features_path):
shutil.copytree(srccode_features_path, copy_features_path)
else:
print(
"Bimtester source code features directory {} not found."
.format(srccode_features_path)
)
return False
# copy features files
# print(the_features_path)
# print(copy_features_path)
# parameter dirs_exist_ok=True, from py 3.8
# if os.path.exists(the_features_path):
# shutil.copytree(
# the_features_path,
# copy_features_path,
# dirs_exist_ok=True
# )
# copy feature files
feature_files = os.listdir(the_features_path)
# print(feature_files)
for feature_file in feature_files:
cp_feature_file = os.path.join(copy_features_path, feature_file)
# print(feature_file)
# copy file
shutil.copyfile(
os.path.join(the_features_path, feature_file),
cp_feature_file
)
return args, copy_base_path
def run_all(args):
print("# Run all.")
# run bimtester
report_file = run_tests(args)
print(report_file)
# check if it worked out well
if report_file is False:
print("BIMTester behave tests returned False.")
return False
if not os.path.isfile(report_file):
print("Report directory does not exist. This should not happen. Debug")
return False
# create html report and open in webbrowser
from .reports import generate_report
generate_report(report_file=report_file)
# get the feature files
feature_files = os.listdir(
os.path.join(args["featuresdir"], "features")
)
# print(feature_files)
for ff in feature_files:
webbrowser.open(os.path.join(
os.path.dirname(report_file),
ff + ".html"
))
return True
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env python3
import os
import argparse
import bimtester.clean
import bimtester.reports
import bimtester.run
parser = argparse.ArgumentParser(description="Runs unit tests for BIM data")
parser.add_argument("-a", "--action", type=str, help="Action to perform, from run/purge", default="run")
parser.add_argument("--advanced-arguments", type=str, help="Specify arguments to Behave", default="")
parser.add_argument("-c", "--console", action="store_true", help="Show results in the console")
parser.add_argument("-f", "--feature", type=str, help="Specify a feature file to test", required=True)
parser.add_argument("-i", "--ifc", type=str, help="Specify a ifc file", required=True)
parser.add_argument("-p", "--path", type=str, help="Define a path for use in tests")
parser.add_argument("-r", "--report", type=str, help="Specify an output file for a HTML report")
parser.add_argument("--lang", type=str, help="Specify a language", default="")
args = vars(parser.parse_args())
if args["action"] == "run":
report_json = bimtester.run.TestRunner(args["ifc"]).run(args)
if args["report"]:
bimtester.reports.ReportGenerator().generate(report_json, args["report"])
elif args["action"] == "purge":
bimtester.clean.TestPurger().purge()
print("# All tasks are complete :-)")
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env python3
from bimtester.guiwidget import run
run()
-163
View File
@@ -1,163 +0,0 @@
#!/usr/bin/env python3
import argparse
import os
from bimtester import clean
from bimtester import reports
from bimtester import run
def show_widget(
features="",
ifcfile="",
get_featurepath_from_ifcpath=False,
args=[]
):
import sys
from PySide2 import QtWidgets
from bimtester.guiwidget import GuiWidgetBimTester
# Create the Qt Application
app = QtWidgets.QApplication(sys.argv)
# Create and show the form
form = GuiWidgetBimTester(
features,
ifcfile,
get_featurepath_from_ifcpath,
args
)
form.show()
# Run the main Qt loop
sys.exit(app.exec_())
if __name__ == "__main__":
# TODO make similar to bash commands
# use - not _ in named args
parser = argparse.ArgumentParser(
description="Runs unit tests for BIM data"
)
parser.add_argument(
"-a",
"--advanced-arguments",
type=str,
help="Specify your own arguments to Python's Behave",
default=""
)
parser.add_argument(
"-c",
"--console",
action="store_true",
help="Show results in the console"
)
parser.add_argument(
"-d",
"--featuresdir",
type=str,
help=(
"Specify a features directory. This should contain "
"a directory named 'features' which contains all the "
"feature files."
),
default=""
)
parser.add_argument(
"-f",
"--feature",
type=str,
help="Specify a feature file to test",
default=""
)
parser.add_argument(
"-g",
"--gui",
action="store_true",
help=(
"Start the gui. The option t (copyintemprun) "
"is triggered automaticly."
)
)
parser.add_argument(
"-i",
"--ifcfile",
type=str,
help=(
"Specify a ifc file."
),
default=""
)
parser.add_argument(
"-p",
"--purge",
action="store_true",
help="Purge tests of deleted elements"
)
parser.add_argument(
"-path",
"--path",
type=str,
help=(
"Specify a path to prepend to feature and ifc file"
),
default=""
)
parser.add_argument(
"-r",
"--report",
action="store_true",
help="Generate a HTML report"
)
parser.add_argument(
"-rr",
"--report_after_run",
action="store_true",
help="Generate a HTML report after running the tests"
)
parser.add_argument(
"-t",
"--copyintemprun",
action="store_true",
help=(
"Copy steps and feature files into a temporary directory "
"and run bimtester with them."
)
)
args = vars(parser.parse_args())
from json import dumps
print(dumps(args, indent=4))
if args["path"]:
if args["feature"]:
args["feature"] = os.path.join(args["path"], args["feature"])
if not args["gui"]:
args["ifcfile"] = os.path.join(args["path"], args["ifcfile"])
if args["purge"]:
clean.TestPurger().purge()
elif args["report"]:
reports.generate_report()
elif args["gui"]:
args["copyintemprun"] = True
fea = args["featuresdir"]
ifc = args["ifcfile"]
if fea != "" and ifc != "":
show_widget(fea, ifc, False, args)
elif fea == "" and ifc != "":
show_widget(fea, ifc, True, args)
elif args["copyintemprun"]:
run.run_tests(args)
# TODO merge with else, but do not forget the tmp dir is not known
else:
run.run_tests(args)
if args["report_after_run"]:
reports.generate_report()
print("# All tasks are complete :-)")