mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
BIMTester now operates on single feature files, which heavily improves usability
This commit is contained in:
+110
-17
@@ -1,29 +1,122 @@
|
||||
# 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
|
||||
+ 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`
|
||||
Languages supported include (in alphabetical order):
|
||||
|
||||
* 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 .
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
from behave.model import Scenario
|
||||
|
||||
from logfile import create_logfile
|
||||
from logfile import append_logfile
|
||||
from zoom_smart_view import append_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__))
|
||||
|
||||
|
||||
def before_all(context):
|
||||
|
||||
# get from userdata
|
||||
userdata = context.config.userdata
|
||||
context.localedir = userdata.get("localedir")
|
||||
context.ifcfile = userdata["ifcfile"]
|
||||
context.ifcbasename = os.path.basename(
|
||||
os.path.splitext(context.ifcfile)[0]
|
||||
|
||||
if context.config.lang:
|
||||
switch_locale(userdata.get("localedir"), context.config.lang)
|
||||
|
||||
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(
|
||||
"runner.continue_after_failed_step", True
|
||||
)
|
||||
@@ -37,21 +37,21 @@ def before_all(context):
|
||||
# set up log file
|
||||
context.thelogfile = os.path.join(
|
||||
context.outpath,
|
||||
context.ifcbasename + ".log"
|
||||
context.ifc_basename + ".log"
|
||||
)
|
||||
create_logfile(
|
||||
context.thelogfile,
|
||||
context.ifcbasename,
|
||||
context.ifc_basename,
|
||||
)
|
||||
|
||||
# set up smart view file
|
||||
context.smview_file = os.path.join(
|
||||
context.outpath,
|
||||
context.ifcbasename + ".bcsv"
|
||||
context.ifc_basename + ".bcsv"
|
||||
)
|
||||
create_zoom_smartview(
|
||||
context.smview_file,
|
||||
context.ifcbasename,
|
||||
context.ifc_basename,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import gettext # noqa
|
||||
import gettext
|
||||
from behave import given
|
||||
from behave import step
|
||||
|
||||
import ifcdata_methods as idm
|
||||
from utils import IfcFile
|
||||
from utils import switch_locale
|
||||
|
||||
|
||||
the_lang = "en"
|
||||
|
||||
from bimtester.ifc import IfcStore
|
||||
from bimtester.lang import _
|
||||
|
||||
@step('The IFC schema "{schema}" must be provided')
|
||||
def step_impl(context, schema):
|
||||
try:
|
||||
if context.config.userdata.get('path'):
|
||||
schema = os.path.join(context.config.userdata.get('path'), schema)
|
||||
if context.config.userdata.get("path"):
|
||||
schema = os.path.join(context.config.userdata.get("path"), schema)
|
||||
IfcFile.load_schema(schema)
|
||||
except:
|
||||
assert False, f"The schema {schema} could not be loaded"
|
||||
@@ -28,25 +24,11 @@ def step_impl(context, file):
|
||||
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")
|
||||
def step_impl(context, schema):
|
||||
switch_locale(context.localedir, the_lang)
|
||||
idm.has_ifcdata_specific_schema(context, schema)
|
||||
|
||||
real_schema = IfcStore.file.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')
|
||||
def step_impl(context, file):
|
||||
@@ -61,41 +43,43 @@ def step_impl(context, reason):
|
||||
@step("The IFC file must be exported by application full name {fullname}")
|
||||
def step_impl(context, fullname):
|
||||
|
||||
real_fullname = IfcFile.get().by_type("IfcApplication")[0].ApplicationFullName
|
||||
assert real_fullname == fullname , (
|
||||
real_fullname = IfcStore.file.by_type("IfcApplication")[0].ApplicationFullName
|
||||
assert real_fullname == fullname, (
|
||||
"The IFC file was not exported by application full name {} "
|
||||
"instead it was exported by application full name {}"
|
||||
.format(fullname, real_fullname)
|
||||
"instead it was exported by application full name {}".format(fullname, real_fullname)
|
||||
)
|
||||
|
||||
|
||||
@step("The IFC file must be exported by application identifier {identifier}")
|
||||
def step_impl(context, identifier):
|
||||
|
||||
real_identifier = IfcFile.get().by_type("IfcApplication")[0].ApplicationIdentifier
|
||||
assert real_identifier == identifier , (
|
||||
"The IFC file was not exported by application identifier {} "
|
||||
"instead it was exported by identifier {}"
|
||||
.format(identifier, real_identifier)
|
||||
real_identifier = IfcStore.file.by_type("IfcApplication")[0].ApplicationIdentifier
|
||||
assert (
|
||||
real_identifier == identifier
|
||||
), "The IFC file was not exported by application identifier {} " "instead it was exported by identifier {}".format(
|
||||
identifier, real_identifier
|
||||
)
|
||||
|
||||
|
||||
@step("The IFC file must be exported by the application version {version}")
|
||||
def step_impl(context, version):
|
||||
|
||||
real_version = IfcFile.get().by_type("IfcApplication")[0].Version
|
||||
assert real_version == version , (
|
||||
"The IFC file was not exported by application version {} "
|
||||
"instead it was exported by version {}"
|
||||
.format(version, real_version)
|
||||
real_version = IfcStore.file.by_type("IfcApplication")[0].Version
|
||||
assert (
|
||||
real_version == version
|
||||
), "The IFC file was not exported by application version {} " "instead it was exported by version {}".format(
|
||||
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):
|
||||
|
||||
is_header_file_description = IfcFile.get().wrapped_data.header.file_description.description
|
||||
assert str(is_header_file_description) == header_file_description , (
|
||||
"The file was not exported by the new ifc exporter in Allplan. File description header: {}"
|
||||
.format(is_header_file_description)
|
||||
|
||||
is_header_file_description = IfcStore.file.wrapped_data.header.file_description.description
|
||||
assert (
|
||||
str(is_header_file_description) == 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
|
||||
|
||||
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")
|
||||
def step_impl(context, schema):
|
||||
switch_locale(context.localedir, the_lang)
|
||||
idm.has_ifcdata_specific_schema(context, schema)
|
||||
context.execute_steps(f"* IFC data must use the {schema} schema")
|
||||
|
||||
@@ -1,22 +1,6 @@
|
||||
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}")
|
||||
def step_impl(context, schema):
|
||||
switch_locale(context.localedir, the_lang)
|
||||
idm.has_ifcdata_specific_schema(context, schema)
|
||||
context.execute_steps(f"* IFC data must use the {schema} schema")
|
||||
|
||||
@@ -1,19 +1,6 @@
|
||||
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}")
|
||||
def step_impl(context, schema):
|
||||
switch_locale(context.localedir, the_lang)
|
||||
idm.has_ifcdata_specific_schema(context, schema)
|
||||
context.execute_steps(f"* IFC data must use the {schema} 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
|
||||
|
||||
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")
|
||||
def step_impl(context, schema):
|
||||
switch_locale(context.localedir, the_lang)
|
||||
idm.has_ifcdata_specific_schema(context, schema)
|
||||
context.execute_steps(f"* IFC data must use the {schema} schema")
|
||||
|
||||
@@ -2,36 +2,37 @@ from behave import step
|
||||
|
||||
from utils import assert_attribute
|
||||
from utils import IfcFile
|
||||
from bimtester.ifc import IfcStore
|
||||
|
||||
|
||||
@step("The project must have an identifier of {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}"')
|
||||
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}"')
|
||||
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}"')
|
||||
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}"')
|
||||
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')
|
||||
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")
|
||||
|
||||
@@ -4,7 +4,6 @@ import ifcopenshell.express
|
||||
import ifcopenshell.util
|
||||
import ifcopenshell.util.element
|
||||
|
||||
|
||||
class IfcFile(object):
|
||||
file = None
|
||||
bookmarks = {}
|
||||
@@ -14,7 +13,7 @@ class IfcFile(object):
|
||||
cls.file = ifcopenshell.open(path)
|
||||
if not cls.file:
|
||||
assert False
|
||||
|
||||
|
||||
@classmethod
|
||||
def load_schema(cls, path=None):
|
||||
schema = ifcopenshell.express.parse(path)
|
||||
@@ -32,7 +31,7 @@ class IfcFile(object):
|
||||
return cls.get().by_guid(guid)
|
||||
except:
|
||||
assert False, "An element with the ID {} could not be found.".format(guid)
|
||||
|
||||
|
||||
@classmethod
|
||||
def by_type(cls, ifc_type):
|
||||
return cls.get().by_type(ifc_type.strip())
|
||||
@@ -108,60 +107,36 @@ def assert_elements(
|
||||
message_all_falseelems,
|
||||
message_some_falseelems,
|
||||
message_no_elems,
|
||||
parameter=None
|
||||
parameter=None,
|
||||
):
|
||||
if elemcount > 0 and falsecount == 0:
|
||||
return # Test OK
|
||||
elif elemcount == 0:
|
||||
assert False, (
|
||||
message_no_elems.format(
|
||||
ifc_class=ifc_class
|
||||
)
|
||||
)
|
||||
assert False, message_no_elems.format(ifc_class=ifc_class)
|
||||
elif falsecount == elemcount:
|
||||
if parameter is None:
|
||||
assert False, (
|
||||
message_all_falseelems.format(
|
||||
elemcount=elemcount,
|
||||
ifc_class=ifc_class
|
||||
)
|
||||
)
|
||||
assert False, message_all_falseelems.format(elemcount=elemcount, ifc_class=ifc_class)
|
||||
else:
|
||||
assert False, (
|
||||
message_all_falseelems.format(
|
||||
elemcount=elemcount,
|
||||
ifc_class=ifc_class,
|
||||
parameter=parameter
|
||||
)
|
||||
)
|
||||
assert False, message_all_falseelems.format(elemcount=elemcount, ifc_class=ifc_class, parameter=parameter)
|
||||
elif falsecount > 0 and falsecount < elemcount:
|
||||
if parameter is None:
|
||||
assert False, (
|
||||
message_some_falseelems.format(
|
||||
falsecount=falsecount,
|
||||
elemcount=elemcount,
|
||||
ifc_class=ifc_class,
|
||||
falseelems=falseelems,
|
||||
)
|
||||
assert False, message_some_falseelems.format(
|
||||
falsecount=falsecount,
|
||||
elemcount=elemcount,
|
||||
ifc_class=ifc_class,
|
||||
falseelems=falseelems,
|
||||
)
|
||||
else:
|
||||
assert False, (
|
||||
message_some_falseelems.format(
|
||||
falsecount=falsecount,
|
||||
elemcount=elemcount,
|
||||
ifc_class=ifc_class,
|
||||
falseelems=falseelems,
|
||||
parameter=parameter
|
||||
)
|
||||
assert False, message_some_falseelems.format(
|
||||
falsecount=falsecount,
|
||||
elemcount=elemcount,
|
||||
ifc_class=ifc_class,
|
||||
falseelems=falseelems,
|
||||
parameter=parameter,
|
||||
)
|
||||
else:
|
||||
assert False, _("Error in falsecount, something went wrong.")
|
||||
|
||||
|
||||
def switch_locale(locale_dir, locale_id="en"):
|
||||
newlang = gettext.translation(
|
||||
"messages",
|
||||
localedir=locale_dir,
|
||||
languages=[locale_id]
|
||||
)
|
||||
newlang.install()
|
||||
pass
|
||||
|
||||
@@ -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 sys
|
||||
import bimtester.run
|
||||
from PySide2 import QtCore
|
||||
from PySide2 import QtGui
|
||||
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):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
featurespath="",
|
||||
ifcfile="",
|
||||
get_featurepath_from_ifcpath=False,
|
||||
args=[]
|
||||
):
|
||||
def __init__(self, args=[]):
|
||||
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
|
||||
# print(self.initial_featurespath)
|
||||
# print(self.initial_ifcfile)
|
||||
# print(self.get_featurepath_from_ifcpath)
|
||||
|
||||
# init ui
|
||||
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
|
||||
# http://forum.freecadweb.org/viewtopic.php?f=18&t=10732&start=10#p86493
|
||||
def __del__(self):
|
||||
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"
|
||||
)
|
||||
iconpath = os.path.join(package_path, "resources", "icons", "bimtester.ico")
|
||||
|
||||
"""
|
||||
# as svg
|
||||
# https://stackoverflow.com/a/35138314
|
||||
@@ -78,6 +40,7 @@ class GuiWidgetBimTester(QtWidgets.QWidget):
|
||||
#)
|
||||
#theicon.sizeHint()
|
||||
"""
|
||||
|
||||
# as pixmap
|
||||
theicon = QtWidgets.QLabel(self)
|
||||
iconpixmap = QtGui.QPixmap(iconpath)
|
||||
@@ -87,7 +50,6 @@ class GuiWidgetBimTester(QtWidgets.QWidget):
|
||||
# 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)
|
||||
@@ -95,36 +57,20 @@ class GuiWidgetBimTester(QtWidgets.QWidget):
|
||||
# feature files path
|
||||
# use a layout with a frame and a title, see solver framework tp
|
||||
# beside button
|
||||
ffifc_str = (
|
||||
"Feature files in a directory 'features' beside the IFC file."
|
||||
)
|
||||
ffifc_str = "Feature files in a directory 'features' beside the IFC file."
|
||||
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. "
|
||||
"'features' directory has to be in there."
|
||||
)
|
||||
_ffdir_str = "Feature files directory. " "'features' directory has to be in there."
|
||||
_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
|
||||
)
|
||||
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 = 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()
|
||||
@@ -136,7 +82,6 @@ class GuiWidgetBimTester(QtWidgets.QWidget):
|
||||
layout.addWidget(theicon, 1, 0, alignment=QtCore.Qt.AlignRight)
|
||||
|
||||
layout.addWidget(featuredirfromifc_label, 2, 0)
|
||||
layout.addWidget(self.featuredirfromifc_cb, 2, 1)
|
||||
|
||||
layout.addWidget(_featurefilesdir_label, 3, 0)
|
||||
layout.addWidget(self.featurefilesdir_text, 4, 0)
|
||||
@@ -153,17 +98,8 @@ class GuiWidgetBimTester(QtWidgets.QWidget):
|
||||
layout.setRowStretch(0, 10)
|
||||
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):
|
||||
# print(self.get_ifcfile())
|
||||
# print(os.path.isfile(self.get_ifcfile()))
|
||||
ifcfile = QtWidgets.QFileDialog.getOpenFileName(
|
||||
self,
|
||||
dir=self.get_ifcfile()
|
||||
)[0]
|
||||
ifcfile = QtWidgets.QFileDialog.getOpenFileName(self, dir=self.get_ifcfile())[0]
|
||||
self.set_ifcfile(ifcfile)
|
||||
|
||||
def set_ifcfile(self, a_file):
|
||||
@@ -172,26 +108,13 @@ class GuiWidgetBimTester(QtWidgets.QWidget):
|
||||
def get_ifcfile(self):
|
||||
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):
|
||||
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
|
||||
options=QtWidgets.QFileDialog.HideNameFilterDetails,
|
||||
)
|
||||
self.set_featurefilesdir(features_path)
|
||||
|
||||
@@ -201,46 +124,11 @@ class GuiWidgetBimTester(QtWidgets.QWidget):
|
||||
def get_featurefilesdir(self):
|
||||
return self.featurefilesdir_text.text()
|
||||
|
||||
# **********************************************************
|
||||
def run_bimtester(self):
|
||||
print("Run BIMTester by the GUI")
|
||||
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor)
|
||||
|
||||
# get features dir
|
||||
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()
|
||||
the_features_path = self.get_featurefilesdir()
|
||||
print(the_features_path)
|
||||
|
||||
# get ifc file
|
||||
@@ -252,14 +140,11 @@ class GuiWidgetBimTester(QtWidgets.QWidget):
|
||||
patched_args["featuresdir"] = the_features_path
|
||||
patched_args["ifcfile"] = the_ifcfile
|
||||
|
||||
# run bimtester
|
||||
status = run_all(patched_args)
|
||||
print(status)
|
||||
bimtester.run.TestRunner("file.ifc").run({})
|
||||
|
||||
QtWidgets.QApplication.restoreOverrideCursor()
|
||||
|
||||
def close_widget(self):
|
||||
print("Close BIMTester Gui")
|
||||
self.close()
|
||||
|
||||
def closeEvent(self, ev):
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
class IfcStore:
|
||||
path = ""
|
||||
file = None
|
||||
@@ -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
|
||||
@@ -1,58 +1,26 @@
|
||||
import datetime
|
||||
import gettext # noqa
|
||||
import json
|
||||
import os
|
||||
import pystache
|
||||
|
||||
from .features.steps.utils import switch_locale
|
||||
from bimtester.lang import _
|
||||
|
||||
|
||||
def generate_report(
|
||||
report_dir=".",
|
||||
use_report_folder=True,
|
||||
report_file_name="report.json",
|
||||
html_template_file_path="",
|
||||
report_file=""
|
||||
):
|
||||
class ReportGenerator:
|
||||
def __init__(self):
|
||||
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__))
|
||||
|
||||
# TODO use far less parameter
|
||||
# to be discussed with other devs
|
||||
def generate(self, report_json, output_file):
|
||||
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
|
||||
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:
|
||||
def generate_feature_report(self, feature, output_file):
|
||||
file_name = os.path.basename(feature["location"]).split(":")[0]
|
||||
data = {
|
||||
"file_name": file_name,
|
||||
@@ -62,108 +30,101 @@ def generate_report(
|
||||
"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
|
||||
return
|
||||
|
||||
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"], "<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,
|
||||
}
|
||||
)
|
||||
scenario_data = self.process_scenario(scenario)
|
||||
if scenario_data:
|
||||
data["scenarios"].append(scenario_data)
|
||||
|
||||
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)
|
||||
|
||||
# translate report
|
||||
# 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)
|
||||
data.update(self.get_template_strings())
|
||||
|
||||
html_report = os.path.join(report_dir, "{}.html".format(file_name))
|
||||
html_tmpl = os.path.join(report_template_path, "template.html")
|
||||
with open(html_report, "w", encoding="utf8") as out:
|
||||
with open(html_tmpl, encoding="utf8") as template:
|
||||
with open(output_file, "w", encoding="utf8") as out:
|
||||
with open(
|
||||
os.path.join(self.base_path, "resources", "reports", "template.html"), encoding="utf8"
|
||||
) as template:
|
||||
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 {
|
||||
"tr_lang": _("en"),
|
||||
"tr_success": _("Success"),
|
||||
"tr_failure": _("Failure"),
|
||||
"tr_tests_passed": _("Tests passed"),
|
||||
"tr_duration": _("Duration"),
|
||||
"tr_auditing": _("OpenBIM auditing is a feature of"),
|
||||
"tr_and": _("and")
|
||||
}
|
||||
for step in scenario["steps"]:
|
||||
step_data = self.process_step(step)
|
||||
total_duration += step_data["time_raw"]
|
||||
steps.append(step_data)
|
||||
|
||||
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)
|
||||
|
||||
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>
|
||||
<html lang={{tr_lang}}>
|
||||
<html lang={{_lang}}>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -32,8 +32,8 @@
|
||||
<h1>{{name}}</h1>
|
||||
<p><strong>{{time}} {{file_name}}</strong></p>
|
||||
<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>
|
||||
{{tr_tests_passed}}: <strong>{{total_passes}} / {{total_steps}}</strong> ({{pass_rate}}%)
|
||||
<span class="{{#is_success}}success{{/is_success}}{{^is_success}}failure{{/is_success}}">{{#is_success}}{{_success}}{{/is_success}}{{^is_success}}{{_failure}}{{/is_success}}</span>
|
||||
{{_tests_passed}}: <strong>{{total_passes}} / {{total_steps}}</strong> ({{pass_rate}}%)
|
||||
<br />
|
||||
<p class="description">
|
||||
{{#description}}
|
||||
@@ -46,10 +46,10 @@
|
||||
<section>
|
||||
<h2>{{name}}</h2>
|
||||
<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>
|
||||
{{tr_tests_passed}}: <strong>{{total_passes}} / {{total_steps}}</strong> ({{pass_rate}}%)
|
||||
<span class="{{#is_success}}success{{/is_success}}{{^is_success}}failure{{/is_success}}">{{#is_success}}{{_success}}{{/is_success}}{{^is_success}}{{_failure}}{{/is_success}}</span>
|
||||
{{_tests_passed}}: <strong>{{total_passes}} / {{total_steps}}</strong> ({{pass_rate}}%)
|
||||
<span class="time">
|
||||
{{tr_duration}}: {{time}}s
|
||||
{{_duration}}: {{time}}s
|
||||
</span>
|
||||
</p>
|
||||
<ol>
|
||||
@@ -72,7 +72,7 @@
|
||||
<hr>
|
||||
<footer>
|
||||
<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>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
@@ -1,343 +1,58 @@
|
||||
import behave.formatter.pretty # Needed for pyinstaller to package it
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import shutil
|
||||
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
|
||||
# like German Umlaute behave gives an error
|
||||
class TestRunner:
|
||||
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
|
||||
bimtester_path = os.path.dirname(os.path.realpath(__file__))
|
||||
# print(bimtester_path)
|
||||
locale_path = os.path.join(bimtester_path, "locale")
|
||||
self.locale_path = os.path.join(self.base_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:
|
||||
# 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):
|
||||
def get_behave_args(self, args, features_path, report_json):
|
||||
behave_args = [features_path]
|
||||
else:
|
||||
return []
|
||||
|
||||
if os.path.isdir(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)
|
||||
)
|
||||
behave_args.extend(["--define", "localedir={}".format(self.locale_path)])
|
||||
|
||||
if args["advanced_arguments"]:
|
||||
behave_args.extend(args["advanced_arguments"].split())
|
||||
if args["advanced_arguments"]:
|
||||
behave_args.extend(args["advanced_arguments"].split())
|
||||
|
||||
if args["ifcfile"]:
|
||||
behave_args.extend([
|
||||
# next two lines are one arg
|
||||
"--define",
|
||||
"ifcfile={}".format(args["ifcfile"])
|
||||
])
|
||||
if args["ifc"]:
|
||||
behave_args.extend(["--define", "ifc={}".format(args["ifc"])])
|
||||
|
||||
if args["path"]:
|
||||
behave_args.extend([
|
||||
# next two lines are one arg
|
||||
"--define",
|
||||
"path={}".format(args["path"])
|
||||
])
|
||||
if args["path"]:
|
||||
behave_args.extend(["--define", "path={}".format(args["path"])])
|
||||
|
||||
if not args["console"]:
|
||||
behave_args.extend([
|
||||
# redirect prints in step methods
|
||||
# if step fails some output is catched, thus might not be printed
|
||||
if args["lang"]:
|
||||
behave_args.extend(["--lang={}".format(args["lang"])])
|
||||
|
||||
if not args["console"]:
|
||||
# https://github.com/behave/behave/issues/346
|
||||
"--no-capture",
|
||||
# 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,
|
||||
])
|
||||
behave_args.extend(["--no-capture", "--format", "json.pretty", "--outfile", report_json])
|
||||
|
||||
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
|
||||
return behave_args
|
||||
|
||||
Executable
+28
@@ -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 :-)")
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from bimtester.guiwidget import run
|
||||
|
||||
|
||||
run()
|
||||
@@ -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 :-)")
|
||||
Reference in New Issue
Block a user