Skip to content

Commit 82c38f6

Browse files
committed
BIMTester can now package test definitions for portability
1 parent 4c58cec commit 82c38f6

2 files changed

Lines changed: 80 additions & 45 deletions

File tree

src/ifcbimtester/bimtester.py

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
# This can be packaged with `pyinstaller --onefile --clean --icon=icon.ico bimtester.py`
1+
# Unix:
2+
# $ pyinstaller --onefile --clean --icon=icon.ico --add-data "features:features" bimtester.py`
3+
# Windows:
4+
# $ pyinstaller --onefile --clean --icon=icon.ico --add-data "features;features" bimtester.py`
25

36
from behave.__main__ import main as behave_main
47
import behave.formatter.pretty # Needed for pyinstaller to package it
@@ -11,26 +14,59 @@
1114
import argparse
1215
import csv
1316
import re
17+
import shutil
1418
from pathlib import Path
1519

20+
21+
def get_resource_path(relative_path):
22+
try:
23+
# PyInstaller creates a temp folder and stores path in _MEIPASS
24+
base_path = sys._MEIPASS
25+
except Exception:
26+
base_path = os.path.abspath(".")
27+
return os.path.join(base_path, relative_path)
28+
29+
1630
def run_tests(args):
17-
behave_args = [args.feature, '--junit', '--junit-directory', args.junit_directory]
31+
if not get_features(args):
32+
sys.exit('No requirements could be found to check.')
33+
behave_args = [get_resource_path('features')]
1834
if args.advanced_arguments:
19-
behave_args = args.advanced_arguments.split()
35+
behave_args.extend(args.advanced_arguments.split())
36+
else:
37+
behave_args.extend(['--junit', '--junit-directory', args.junit_directory])
2038
behave_main(behave_args)
2139
print('# All tests are finished.')
2240

41+
42+
def get_features(args):
43+
has_features = False
44+
if os.path.exists('features'):
45+
shutil.copytree('features', get_resource_path('features'))
46+
has_features = True
47+
for f in os.listdir('.'):
48+
if not f.endswith('.requirement'):
49+
continue
50+
if args.feature and args.feature != f:
51+
continue
52+
has_features = True
53+
shutil.copyfile(f, os.path.join(
54+
get_resource_path('features'),
55+
os.path.basename(f)[0:-len('.requirement')] + '.feature'))
56+
return has_features
57+
58+
2359
def generate_report(args):
2460
print('# Generating HTML reports now.')
2561
if not os.path.exists('report'):
2662
os.mkdir('report')
2763
if not os.path.exists(args.junit_directory):
2864
os.mkdir(args.junit_directory)
29-
for file in os.listdir(args.junit_directory):
30-
if not file.endswith('.xml'):
65+
for f in os.listdir(args.junit_directory):
66+
if not f.endswith('.xml'):
3167
continue
32-
print(f'Processing {file} ...')
33-
root = ET.parse('{}{}'.format(args.junit_directory, file)).getroot()
68+
print(f'Processing {f} ...')
69+
root = ET.parse('{}{}'.format(args.junit_directory, f)).getroot()
3470
data = {
3571
'report_name': root.get('name'),
3672
'testcases': []
@@ -39,13 +75,17 @@ def generate_report(args):
3975
steps = []
4076
system_out = testcase.findall('system-out')[0].text.splitlines()
4177
for line in system_out:
42-
if line.strip()[0:4] in ['Give', 'Then', 'When', 'And ']:
78+
if line.strip()[0:4] in ['Give', 'Then', 'When', 'And '] \
79+
or line.strip()[0:2] == '* ':
4380
is_success = True if ' ... passed in ' in line else False
81+
name, time = line.strip().split(' ... ')
82+
if name[0:2] == '* ':
83+
name = name[2:]
4484
steps.append({
45-
'name': line.strip().split(' ... ')[0],
46-
'time': line.strip().split(' ... ')[1],
85+
'name': name,
86+
'time': time,
4787
'is_success': is_success
48-
})
88+
})
4989
total_passes = len([s for s in steps if s['is_success'] == True])
5090
total_steps = len(steps)
5191
pass_rate = round((total_passes / total_steps) * 100)
@@ -58,8 +98,8 @@ def generate_report(args):
5898
'total_steps': total_steps,
5999
'pass_rate': pass_rate
60100
})
61-
with open('report/{}.html'.format(file[0:-4]), 'w') as out:
62-
with open('features/template.html') as template:
101+
with open('report/{}.html'.format(f[0:-4]), 'w') as out:
102+
with open(get_resource_path('features/template.html')) as template:
63103
out.write(pystache.render(template.read(), data))
64104

65105

@@ -98,6 +138,7 @@ def does_global_id_exist(self, global_id):
98138
except:
99139
return False
100140

141+
101142
parser = argparse.ArgumentParser(
102143
description='Runs unit tests for BIM data')
103144
parser.add_argument(
@@ -120,8 +161,8 @@ def does_global_id_exist(self, global_id):
120161
'-f',
121162
'--feature',
122163
type=str,
123-
help='Specify a feature to test',
124-
default='features')
164+
help='Specify a requirements feature file to test',
165+
default='')
125166
parser.add_argument(
126167
'-a',
127168
'--advanced-arguments',
@@ -130,12 +171,6 @@ def does_global_id_exist(self, global_id):
130171
default='')
131172
args = parser.parse_args()
132173

133-
if not os.path.exists('features'):
134-
sys.exit('''
135-
BIMTester requires a features folder to exist within the current folder.
136-
Visit https://blenderbim.org/ to learn more about how to use BIMTester.
137-
''')
138-
139174
if args.purge:
140175
TestPurger().purge()
141176
elif args.report:

src/ifcbimtester/features/steps/steps.py

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import ifcopenshell
2-
from behave import given, when, then, step
2+
from behave import step
33

44
class IfcFile(object):
55
file = None
@@ -31,47 +31,47 @@ def get_property(cls, element, pset_name, property_name):
3131
return prop
3232

3333

34-
@given('the IFC file "{file}"')
34+
@step('The IFC file "{file}" must be provided')
3535
def step_impl(context, file):
3636
IfcFile.load(file)
3737

3838

39-
@given('the IFC file "{file}" exists')
39+
@step('The IFC file "{file}" is exempt from being provided')
4040
def step_impl(context, file):
4141
pass
4242

4343

44-
@then('the file should be an {schema} file')
44+
@step('IFC data must use the {schema} schema')
4545
def step_impl(context, schema):
4646
assert IfcFile.get().schema == schema
4747

4848

49-
@then('the element {id} is an {ifc_class}')
49+
@step('the element {id} is an {ifc_class}')
5050
def step_impl(context, id, ifc_class):
5151
assert IfcFile.get().by_id(id).is_a(ifc_class)
5252

5353

54-
@then('the element {id} should not exist because {reason}')
54+
@step('the element {id} should not exist because {reason}')
5555
def step_impl(context, id, reason):
5656
assert not IfcFile.get().by_id(id)
5757

5858

59-
@then('the file is exempt from auditing because {reason}')
59+
@step('No further requirements are specified because {reason}')
6060
def step_impl(context, reason):
6161
pass
6262

6363

64-
@given(u'there is at least one {ifc_class} element')
64+
@step(u'there is at least one {ifc_class} element')
6565
def step_impl(context, ifc_class):
6666
assert len(IfcFile.get().by_type(ifc_class)) >= 1
6767

6868

69-
@then(u'there are no {ifc_class} elements because {reason}')
69+
@step(u'there are no {ifc_class} elements because {reason}')
7070
def step_impl(context, ifc_class, reason):
7171
assert len(IfcFile.get().by_type(ifc_class)) == 0
7272

7373

74-
@then('all {ifc_class} elements have a name matching the pattern "{pattern}"')
74+
@step('all {ifc_class} elements have a name matching the pattern "{pattern}"')
7575
def step_impl(context, ifc_class, pattern):
7676
import re
7777
elements = IfcFile.get().by_type(ifc_class)
@@ -80,7 +80,7 @@ def step_impl(context, ifc_class, pattern):
8080
assert False
8181

8282

83-
@then('all {ifc_class} elements have an {representation_class} representation')
83+
@step('all {ifc_class} elements have an {representation_class} representation')
8484
def step_impl(context, ifc_class, representation_class):
8585
def is_item_a_representation(item, representation):
8686
if '/' in representation:
@@ -109,15 +109,15 @@ def is_item_a_representation(item, representation):
109109
assert False
110110

111111
use_step_matcher('re')
112-
@then('all (?P<ifc_class>.*) elements have an? (?P<attribute>.*) attribute')
112+
@step('all (?P<ifc_class>.*) elements have an? (?P<attribute>.*) attribute')
113113
def step_impl(context, ifc_class, attribute):
114114
elements = IfcFile.get().by_type(ifc_class)
115115
for element in elements:
116116
if not getattr(element, attribute):
117117
assert False
118118

119119

120-
@then('all (?P<ifc_class>.*) elements have an? (?P<property_path>.*\..*) property')
120+
@step('all (?P<ifc_class>.*) elements have an? (?P<property_path>.*\..*) property')
121121
def step_impl(context, ifc_class, property_path):
122122
pset_name, property_name = property_path.split('.')
123123
elements = IfcFile.get().by_type(ifc_class)
@@ -126,7 +126,7 @@ def step_impl(context, ifc_class, property_path):
126126
assert False
127127

128128

129-
@then('all (?P<ifc_class>.*) elements have an? (?P<property_path>.*\..*) property value matching the pattern "(?P<pattern>.*)"')
129+
@step('all (?P<ifc_class>.*) elements have an? (?P<property_path>.*\..*) property value matching the pattern "(?P<pattern>.*)"')
130130
def step_impl(context, ifc_class, property_path, pattern):
131131
import re
132132
pset_name, property_name = property_path.split('.')
@@ -142,7 +142,7 @@ def step_impl(context, ifc_class, property_path, pattern):
142142
assert False
143143

144144

145-
@then('all (?P<ifc_class>.*) elements have an? (?P<attribute>.*) matching the pattern "(?P<pattern>.*)"')
145+
@step('all (?P<ifc_class>.*) elements have an? (?P<attribute>.*) matching the pattern "(?P<pattern>.*)"')
146146
def step_impl(context, ifc_class, attribute, pattern):
147147
import re
148148
elements = IfcFile.get().by_type(ifc_class)
@@ -152,7 +152,7 @@ def step_impl(context, ifc_class, attribute, pattern):
152152
assert re.search(pattern, value)
153153

154154

155-
@then('all (?P<ifc_class>.*) elements have an? (?P<attributes>.*) taken from the list in "(?P<list_file>.*)"')
155+
@step('all (?P<ifc_class>.*) elements have an? (?P<attributes>.*) taken from the list in "(?P<list_file>.*)"')
156156
def step_impl(context, ifc_class, attributes, list_file):
157157
import csv
158158
values = []
@@ -172,7 +172,7 @@ def step_impl(context, ifc_class, attributes, list_file):
172172

173173

174174
use_step_matcher('parse')
175-
@then('all {ifc_class} elements have a {qto_name}.{quantity_name} quantity')
175+
@step('all {ifc_class} elements have a {qto_name}.{quantity_name} quantity')
176176
def step_impl(context, ifc_class, qto_name, quantity_name):
177177
elements = IfcFile.get().by_type(ifc_class)
178178
for element in elements:
@@ -187,7 +187,7 @@ def step_impl(context, ifc_class, qto_name, quantity_name):
187187
if not is_successful:
188188
assert False
189189

190-
@then(u'the project should have geolocation data')
190+
@step(u'the project should have geolocation data')
191191
def step_impl(context):
192192
if IfcFile.get().schema == 'IFC2X3':
193193
for site in IfcFile.get().by_type('IfcSite'):
@@ -203,7 +203,7 @@ def step_impl(context):
203203
return
204204
assert False
205205

206-
@then(u'the project geolocation uses the "{crs_name}" CRS')
206+
@step(u'the project geolocation uses the "{crs_name}" CRS')
207207
def step_impl(context, crs_name):
208208
if IfcFile.get().schema == 'IFC2X3':
209209
for site in IfcFile.get().by_type('IfcSite'):
@@ -214,24 +214,24 @@ def step_impl(context, crs_name):
214214

215215

216216
use_step_matcher('re')
217-
@then(u'the geolocated datum has an? (?P<attribute>.*) of "(?P<value>.*)"')
217+
@step(u'the geolocated datum has an? (?P<attribute>.*) of "(?P<value>.*)"')
218218
def step_impl(context, attribute, value):
219219
if IfcFile.get().schema == 'IFC2X3':
220220
site = IfcFile.get().by_type('IfcSite')[0]
221221
actual_value = IfcFile.get_property(site, 'EPset_MapConversion', attribute).NominalValue.wrappedValue
222222
else:
223223
actual_value = getattr(IfcFile.get().by_id(IfcFile.bookmarks['geolocation']), attribute)
224-
assert str(actual_value) == value, f'The value was {actual_value}'
224+
assert str(actual_value) == value, f'The value was {actual_value}'
225225

226226

227227
use_step_matcher('parse')
228-
@then(u'the project has a {attribute_name} attribute with a value of "{attribute_value}"')
228+
@step(u'the project has a {attribute_name} attribute with a value of "{attribute_value}"')
229229
def step_impl(context, attribute_name, attribute_value):
230230
project = IfcFile.get().by_type('IfcProject')[0]
231231
assert getattr(project, attribute_name) == attribute_value
232232

233233

234-
@then(u'there is an {ifc_class} element with a {attribute_name} attribute with a value of "{attribute_value}"')
234+
@step(u'there is an {ifc_class} element with a {attribute_name} attribute with a value of "{attribute_value}"')
235235
def step_impl(context, ifc_class, attribute_name, attribute_value):
236236
elements = IfcFile.get().by_type(ifc_class)
237237
for element in elements:
@@ -241,7 +241,7 @@ def step_impl(context, ifc_class, attribute_name, attribute_value):
241241
assert False
242242

243243

244-
@then(u'all buildings have an address')
244+
@step(u'all buildings have an address')
245245
def step_impl(context):
246246
for building in IfcFile.get().by_type('IfcBuilding'):
247247
if not building.BuildingAddress:

0 commit comments

Comments
 (0)