-
Notifications
You must be signed in to change notification settings - Fork 25
/
addon_tests.py
1730 lines (1482 loc) · 54.6 KB
/
addon_tests.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# ##### MCprep #####
#
# Developed by Patrick W. Crawford, see more at
# http://theduckcow.com/dev/blender/MCprep
#
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
import bpy
import traceback
import os
import json
import sys
import io
from contextlib import redirect_stdout
import importlib
import tempfile
import shutil
from mathutils import Vector
TEST_FILE = "test_results.tsv"
# -----------------------------------------------------------------------------
# Primary test loop
# -----------------------------------------------------------------------------
class mcprep_testing():
# {status}, func, prefunc caller (reference only)
def __init__(self):
self.suppress = True # hold stdout
self.test_status = {} # {func.__name__: {"check":-1, "res":-1,0,1}}
self.test_cases = [
self.enable_mcprep,
self.prep_materials,
self.openfolder,
self.spawn_mob,
self.change_skin,
self.import_world_split,
self.import_world_fail,
self.import_jmc2obj,
self.import_mineways_separated,
self.import_mineways_combined,
self.name_generalize,
self.canonical_name_no_none,
self.canonical_test_mappings,
self.meshswap_spawner,
self.meshswap_jmc2obj,
self.meshswap_mineways_separated,
self.meshswap_mineways_combined,
self.detect_desaturated_images,
self.detect_extra_passes,
self.find_missing_images_cycles,
self.qa_meshswap_file,
self.item_spawner,
self.sync_materials,
self.sync_materials_link,
self.load_material,
self.uv_transform_detection,
self.uv_transform_no_alert,
self.uv_transform_combined_alert,
self.world_tools,
]
self.run_only = None # name to give to only run this test
self.mcprep_json = {}
def run_all_tests(self):
"""For use in command line mode, run all tests and checks"""
if self.run_only and self.run_only not in [tst.__name__ for tst in self.test_cases]:
print("{}No tests ran!{} Test function not found: {}".format(
COL.FAIL, COL.ENDC, self.run_only))
for test in self.test_cases:
if self.run_only and test.__name__ != self.run_only:
continue
self.mcrprep_run_test(test)
failed_tests = [tst for tst in self.test_status
if self.test_status[tst]["check"] < 0]
passed_tests = [tst for tst in self.test_status
if self.test_status[tst]["check"] > 0]
print("\n{}COMPLETED, {} passed and {} failed{}".format(
COL.HEADER,
len(passed_tests), len(failed_tests),
COL.ENDC))
if passed_tests:
print("{}Passed tests:{}".format(COL.OKGREEN, COL.ENDC))
print("\t"+", ".join(passed_tests))
if failed_tests:
print("{}Failed tests:{}".format(COL.FAIL, COL.ENDC))
for tst in self.test_status:
if self.test_status[tst]["check"] > 0:
continue
ert = suffix_chars(self.test_status[tst]["res"], 70)
print("\t{}{}{}: {}".format(COL.UNDERLINE, tst, COL.ENDC, ert))
# indicate if all tests passed for this blender version
if not failed_tests:
with open(TEST_FILE, 'a') as tsv:
tsv.write("{}\t{}\t-\n".format(bpy.app.version, "ALL PASSED"))
def write_placeholder(self, test_name):
"""Append placeholder, presuming if not changed then blender crashed"""
with open(TEST_FILE, 'a') as tsv:
tsv.write("{}\t{}\t-\n".format(
bpy.app.version, "CRASH during "+test_name))
def update_placeholder(self, test_name, test_failure):
"""Update text of (if error) or remove placeholder row of file"""
with open(TEST_FILE, 'r') as tsv:
contents = tsv.readlines()
if not test_failure: # None or ""
contents = contents[:-1]
else:
this_failure = "{}\t{}\t{}\n".format(
bpy.app.version, test_name, suffix_chars(test_failure, 20))
contents[-1] = this_failure
with open(TEST_FILE, 'w') as tsv:
for row in contents:
tsv.write(row)
def mcrprep_run_test(self, test_func):
"""Run a single MCprep test"""
print("\n{}Testing {}{}".format(COL.HEADER, test_func.__name__, COL.ENDC))
self.write_placeholder(test_func.__name__)
self._clear_scene()
try:
if self.suppress:
stdout = io.StringIO()
with redirect_stdout(stdout):
res = test_func()
else:
res = test_func()
if not res:
print("\t{}TEST PASSED{}".format(COL.OKGREEN, COL.ENDC))
self.test_status[test_func.__name__] = {"check":1, "res": res}
else:
print("\t{}TEST FAILED:{}".format(COL.FAIL, COL.ENDC))
print("\t"+res)
self.test_status[test_func.__name__] = {"check":-1, "res": res}
except Exception as e:
print("\t{}TEST FAILED{}".format(COL.FAIL, COL.ENDC))
print(traceback.format_exc()) # plus other info, e.g. line number/file?
res = traceback.format_exc()
self.test_status[test_func.__name__] = {"check":-1, "res": res}
# print("\tFinished test {}".format(test_func.__name__))
self.update_placeholder(test_func.__name__, res)
def setup_env_paths(self):
"""Adds the MCprep installed addon path to sys for easier importing."""
to_add = None
for base in bpy.utils.script_paths():
init = os.path.join(base, "addons", "MCprep_addon", "__init__.py")
if os.path.isfile(init):
to_add = init
break
if not to_add:
raise Exception("Could not add the environment path for direct importing")
# add to path and bind so it can use relative improts (3.5 trick)
spec = importlib.util.spec_from_file_location("MCprep", to_add)
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
from MCprep import conf
conf.init()
def get_mcprep_path(self):
"""Returns the addon basepath installed in this blender instance"""
for base in bpy.utils.script_paths():
init = os.path.join(base, "addons", "MCprep_addon") # __init__.py folder
if os.path.isdir(init):
return init
return None
# -----------------------------------------------------------------------------
# Testing utilities, not tests themselves (ie assumed to work)
# -----------------------------------------------------------------------------
def _clear_scene(self):
"""Clear scene and data without printouts"""
# if not self.suppress:
# stdout = io.StringIO()
# with redirect_stdout(stdout):
bpy.ops.wm.read_homefile(app_template="")
for obj in bpy.data.objects:
bpy.data.objects.remove(obj) # wait, that's illegal?
for mat in bpy.data.materials:
bpy.data.materials.remove(mat)
# for txt in bpy.data.texts:
# bpy.data.texts.remove(txt)
def _add_character(self):
"""Add a rigged character to the scene, specifically Alex"""
bpy.ops.mcprep.reload_mobs()
# mcmob_type='player/Simple Rig - Boxscape-TheDuckCow.blend:/:Simple Player'
# mcmob_type='player/Alex FancyFeet - TheDuckCow & VanguardEnni.blend:/:alex'
mcmob_type='hostile/mobs - Rymdnisse.blend:/:silverfish'
bpy.ops.mcprep.mob_spawner(mcmob_type=mcmob_type)
def _import_jmc2obj_full(self):
"""Import the full jmc2obj test set"""
testdir = os.path.dirname(__file__)
obj_path = os.path.join(testdir, "jmc2obj", "jmc2obj_test_1_15_2.obj")
bpy.ops.mcprep.import_world_split(filepath=obj_path)
def _import_mineways_separated(self):
"""Import the full jmc2obj test set"""
testdir = os.path.dirname(__file__)
obj_path = os.path.join(testdir, "mineways", "separated_textures",
"mineways_test_separated_1_15_2.obj")
bpy.ops.mcprep.import_world_split(filepath=obj_path)
def _import_mineways_combined(self):
"""Import the full jmc2obj test set"""
testdir = os.path.dirname(__file__)
obj_path = os.path.join(testdir, "mineways", "combined_textures",
"mineways_test_combined_1_15_2.obj")
bpy.ops.mcprep.import_world_split(filepath=obj_path)
def _create_canon_mat(self, canon=None):
"""Creates a material that should be recognized"""
name = canon if canon else "dirt"
mat = bpy.data.materials.new(name)
mat.use_nodes = True
img_node = mat.node_tree.nodes.new(type="ShaderNodeTexImage")
if canon:
base = self.get_mcprep_path()
filepath = os.path.join(base, "MCprep_resources",
"resourcepacks", "mcprep_default", "assets", "minecraft", "textures",
"block", canon+".png")
img = bpy.data.images.load(filepath)
else:
img = bpy.data.images.new(name, 16, 16)
img_node.image = img
return mat, img_node
# Seems that infolog doesn't update in background mode
def _get_last_infolog(self):
"""Return back the latest info window log"""
for txt in bpy.data.texts:
bpy.data.texts.remove(txt)
res = bpy.ops.ui.reports_to_textblock()
print("DEVVVV get last infolog:")
for ln in bpy.data.texts['Recent Reports'].lines:
print(ln.body)
print("END printlines")
return bpy.data.texts['Recent Reports'].lines[-1].body
def _set_exporter(self, name):
"""Sets the exporter name"""
from MCprep.util import get_user_preferences
if name not in ['(choose)', 'jmc2obj', 'Mineways']:
raise Exception('Invalid exporter set tyep')
context = bpy.context
if hasattr(context, "user_preferences"):
prefs = context.user_preferences.addons.get("MCprep_addon", None)
elif hasattr(context, "preferences"):
prefs = context.preferences.addons.get("MCprep_addon", None)
prefs.preferences.MCprep_exporter_type = name
# -----------------------------------------------------------------------------
# Operator unit tests
# -----------------------------------------------------------------------------
def enable_mcprep(self):
"""Ensure we can both enable and disable MCprep"""
# brute force enable
# stdout = io.StringIO()
# with redirect_stdout(stdout):
try:
if hasattr(bpy.ops, "preferences") and "addon_enable" in dir(bpy.ops.preferences):
bpy.ops.preferences.addon_enable(module="MCprep_addon")
else:
bpy.ops.wm.addon_enable(module="MCprep_addon")
except:
pass
# see if we can safely toggle off and back on
if hasattr(bpy.ops, "preferences") and "addon_enable" in dir(bpy.ops.preferences):
bpy.ops.preferences.addon_disable(module="MCprep_addon")
bpy.ops.preferences.addon_enable(module="MCprep_addon")
else:
bpy.ops.wm.addon_disable(module="MCprep_addon")
bpy.ops.wm.addon_enable(module="MCprep_addon")
def prep_materials(self):
# run once when nothing is selected, no active object
self._clear_scene()
# res = bpy.ops.mcprep.prep_materials(
# animateTextures=False,
# autoFindMissingTextures=False,
# improveUiSettings=False,
# )
# if res != {'CANCELLED'}:
# return "Should have returned cancelled as no objects selected"
# elif "No objects selected" != self._get_last_infolog():
# return "Did not get the right info log back"
status = 'fail'
print("Checking blank usage")
try:
bpy.ops.mcprep.prep_materials(
animateTextures=False,
autoFindMissingTextures=False,
improveUiSettings=False
)
except RuntimeError as e:
if "Error: No objects selected" in str(e):
status = 'success'
else:
return "prep_materials other err: "+str(e)
if status=='fail':
return "Prep should have failed with error on no objects"
# add object, no material. Should still fail as no materials
bpy.ops.mesh.primitive_plane_add()
obj = bpy.context.object
status = 'fail'
try:
res = bpy.ops.mcprep.prep_materials(
animateTextures=False,
autoFindMissingTextures=False,
improveUiSettings=False)
except RuntimeError as e:
if 'No materials found' in str(e):
status = 'success' # expect to fail when nothing selected
if status=='fail':
return "mcprep.prep_materials-02 failure"
# TODO: Add test where material is added but without an image/nodes
# add object with canonical material name. Assume cycles
new_mat, _ = self._create_canon_mat()
obj.active_material = new_mat
status = 'fail'
try:
res = bpy.ops.mcprep.prep_materials(
animateTextures=False,
autoFindMissingTextures=False,
improveUiSettings=False)
except Exception as e:
return "Unexpected error: "+str(e)
# how to tell if prepping actually occured? Should say 1 material prepped
# print(self._get_last_infolog()) # error in 2.82+, not used anyways
def prep_materials_cycles(self):
"""Cycles-specific tests"""
def find_missing_images_cycles(self):
"""Find missing images from selected materials, cycles.
Scenarios in which we find new textures
One: material is empty with no image block assigned at all, though has
image node and material is a canonical name
Two: material has image block but the filepath is missing, find it
Three: image is there, or image is packed; ie assume is fine (don't change)
"""
# first, import a material that has no filepath
self._clear_scene()
mat, node = self._create_canon_mat("sugar_cane")
bpy.ops.mesh.primitive_plane_add()
bpy.context.object.active_material = mat
pre_path = node.image.filepath
bpy.ops.mcprep.replace_missing_textures(animateTextures=False)
post_path = node.image.filepath
canonical_path = post_path # save for later
if pre_path != post_path:
return "Pre/post path differed, should be the same"
# now save the texturefile somewhere
tmp_dir = tempfile.gettempdir()
tmp_image = os.path.join(tmp_dir, "sugar_cane.png")
shutil.copyfile(node.image.filepath, tmp_image) # leave original in tact
# Test that path is unchanged even when with a non canonical path
node.image.filepath = tmp_image
if node.image.filepath != tmp_image:
os.remove(tmp_image)
return "fialed to setup test, node path not = "+tmp_image
pre_path = node.image.filepath
bpy.ops.mcprep.replace_missing_textures(animateTextures=False)
post_path = node.image.filepath
if pre_path != post_path:
os.remove(tmp_image)
return "Pre/post path differed in tmp dir when there should have been no change: pre {} vs post {}".format(
pre_path, post_path)
# test that an empty node within a canonically named material is fixed
pre_path = node.image.filepath
node.image = None # remove the image from block
if node.image:
os.remove(tmp_image)
return "failed to setup test, image block still assigned"
bpy.ops.mcprep.replace_missing_textures(animateTextures=False)
post_path = node.image.filepath
if not post_path:
os.remove(tmp_image)
return "No post path found, should have loaded file"
elif post_path == pre_path:
os.remove(tmp_image)
return "Should have loaded image as new datablock from canon location"
elif not os.path.isfile(post_path):
os.remove(tmp_image)
return "New path file does not exist"
# test an image with broken texturepath is fixed for cannon material name
# node.image.filepath = tmp_image # assert it's not the canonical path
# pre_path = node.image.filepath # the original path before renaming
# os.rename(tmp_image, tmp_image+"x")
# if os.path.isfile(bpy.path.abspath(node.image.filepath)) or pre_path != node.image.filepath:
# os.remove(pre_path)
# os.remove(tmp_image+"x")
# return "Failed to setup test, original file exists/img path updated"
# bpy.ops.mcprep.replace_missing_textures(animateTextures=False)
# post_path = node.image.filepath
# if pre_path == post_path:
# os.remove(tmp_image+"x")
# return "Should have updated missing image to canonical, still is "+post_path
# elif post_path != canonical_path:
# os.remove(tmp_image+"x")
# return "New path not canonical: "+post_path
# os.rename(tmp_image+"x", tmp_image)
# Example where we save and close the blend file, move the file,
# and re-open. First, load the scene
self._clear_scene()
mat, node = self._create_canon_mat("sugar_cane")
bpy.ops.mesh.primitive_plane_add()
bpy.context.object.active_material = mat
# Then, create the textures locally
bpy.ops.file.pack_all()
bpy.ops.file.unpack_all(method='USE_LOCAL')
unpacked_path = bpy.path.abspath(node.image.filepath)
# close and open, moving the file in the meantime
save_tmp_file = os.path.join(tmp_dir, "tmp_test.blend")
os.rename(unpacked_path, unpacked_path+"x")
bpy.ops.wm.save_mainfile(filepath=save_tmp_file)
bpy.ops.wm.open_mainfile(filepath=save_tmp_file)
# now run the operator
img = bpy.data.images['sugar_cane.png']
pre_path = img.filepath
if os.path.isfile(pre_path):
os.remove(unpacked_path+"x")
return "Failed to setup test for save/reopn move"
bpy.ops.mcprep.replace_missing_textures(animateTextures=False)
post_path = img.filepath
if post_path == pre_path:
os.remove(unpacked_path+"x")
return "Did not change path from "+pre_path
elif not os.path.isfile(post_path):
os.remove(unpacked_path+"x")
return "File for blend reloaded image does not exist: "+node.image.filepath
os.remove(unpacked_path+"x")
# address the example of sugar_cane.png.001 not being detected as canonical
# as a front-end name (not image file)
self._clear_scene()
mat, node = self._create_canon_mat("sugar_cane")
bpy.ops.mesh.primitive_plane_add()
bpy.context.object.active_material = mat
pre_path = node.image.filepath
node.image = None # remove the image from block
mat.name = "sugar_cane.png.001"
if node.image:
os.remove(tmp_image)
return "failed to setup test, image block still assigned"
bpy.ops.mcprep.replace_missing_textures(animateTextures=False)
if not node.image:
os.remove(tmp_image)
return "Failed to load new image within mat named .png.001"
post_path = node.image.filepath
if not post_path:
os.remove(tmp_image)
return "No image loaded for "+mat.name
elif not os.path.isfile(node.image.filepath):
return "File for loaded image does not exist: "+node.image.filepath
# Example running with animateTextures too
# check on image that is packed or not, or packed but no data
os.remove(tmp_image)
def openfolder(self):
if bpy.app.background is True:
return "" # can't test this in background mode
folder = bpy.utils.script_path_user()
if not os.path.isdir(folder):
return "Sample folder doesn't exist, couldn't test"
res = bpy.ops.mcprep.openfolder(folder)
if res=={"FINISHED"}:
return ""
else:
return "Failed, returned cancelled"
def spawn_mob(self):
"""Spawn mobs, reload mobs, etc"""
self._clear_scene()
self._add_character() # run the utility as it's own sort of test
self._clear_scene()
bpy.ops.mcprep.reload_mobs()
# sample don't specify mob, just load whatever is first
bpy.ops.mcprep.mob_spawner()
# spawn an alex
# try changing the folder
# try install mob and uninstall
def change_skin(self):
"""Test scenarios for changing skin after adding a character."""
self._clear_scene()
bpy.ops.mcprep.reload_skins()
skin_ind = bpy.context.scene.mcprep_skins_list_index
skin_item = bpy.context.scene.mcprep_skins_list[skin_ind]
tex_name = skin_item['name']
skin_path = os.path.join(bpy.context.scene.mcprep_skin_path, tex_name)
status = 'fail'
try:
res = bpy.ops.mcprep.applyskin(
filepath=skin_path,
new_material=False)
except RuntimeError as e:
if 'No materials found to update' in str(e):
status = 'success' # expect to fail when nothing selected
if status=='fail':
return "Should have failed to skin swap with no objects selected"
# now run on a real test character, with 1 material and 2 objects
self._add_character()
pre_mats = len(bpy.data.materials)
bpy.ops.mcprep.applyskin(
filepath=skin_path,
new_material=False)
post_mats = len(bpy.data.materials)
if post_mats != pre_mats: # should be unchanged
return "change_skin.mat counts diff despit no new mat request, {} before and {} after".format(
pre_mats, post_mats)
# do counts of materials before and after to ensure they match
pre_mats = len(bpy.data.materials)
bpy.ops.mcprep.applyskin(
filepath=skin_path,
new_material=True)
post_mats = len(bpy.data.materials)
if post_mats != pre_mats*2: # should exactly double since in new scene
return "change_skin.mat counts diff mat counts, {} before and {} after".format(
pre_mats, post_mats)
pre_mats = len(bpy.data.materials)
bpy.ops.mcprep.skin_swapper( # not diff operator name, this is popup browser
filepath=skin_path,
new_material=False)
post_mats = len(bpy.data.materials)
if post_mats != pre_mats: # should be unchanged
return "change_skin.mat counts differ even though should be same, {} before and {} after".format(
pre_mats, post_mats)
# TODO: Add test for when there is a bogus filename, responds with
# Image file not found in err
# capture info or recent out?
# check that username was there before or not
bpy.ops.mcprep.applyusernameskin(
username='TheDuckCow',
skip_redownload=False,
new_material=True)
# check that timestamp of last edit of file was longer ago than above cmd
bpy.ops.mcprep.applyusernameskin(
username='TheDuckCow',
skip_redownload=True,
new_material=True)
# test deleting username skin and that file is indeed deleted
# and not in list anymore
# bpy.ops.mcprep.applyusernameskin(
# username='TheDuckCow',
# skip_redownload=True,
# new_material=True)
# test that the file was added back
bpy.ops.mcprep.spawn_with_skin()
# test changing skin to file when no existing images/textres
# test changing skin to file when existing material
# test changing skin to file for both above, cycles and internal
# test changing skin file for both above without, then with,
# then without again, normals + spec etc.
return
def import_world_split(self):
"""Test that imported world has multiple objects"""
self._clear_scene()
pre_objects = len(bpy.data.objects)
self._import_jmc2obj_full()
post_objects = len(bpy.data.objects)
if post_objects+1 > pre_objects:
print("Success, had {} objs, post import {}".format(
pre_objects, post_objects))
return
elif post_objects+1 == pre_objects:
return "Only one new object imported"
else:
return "Nothing imported"
def import_world_fail(self):
"""Ensure loader fails if an invalid path is loaded"""
testdir = os.path.dirname(__file__)
obj_path = os.path.join(testdir, "jmc2obj", "xx_jmc2obj_test_1_14_4.obj")
try:
bpy.ops.mcprep.import_world_split(filepath=obj_path)
except Exception as e:
print("Failed, as intended: "+str(e))
return
return "World import should have returned an error"
def import_materials_util(self, mapping_set):
"""Reusable function for testing on different obj setups"""
from MCprep.materials.generate import get_mc_canonical_name
from MCprep.materials.generate import find_from_texturepack
from MCprep import util
from MCprep import conf
util.load_mcprep_json() # force load json cache
mcprep_data = conf.json_data["blocks"][mapping_set]
# first detect alignment to the raw underlining mappings, nothing to
# do with canonical yet
mapped = [mat.name for mat in bpy.data.materials
if mat.name in mcprep_data] # ok!
unmapped = [mat.name for mat in bpy.data.materials
if mat.name not in mcprep_data] # not ok
fullset = mapped+unmapped # ie all materials
unleveraged = [mat for mat in mcprep_data
if mat not in fullset] # not ideal, means maybe missed check
print("Mapped: {}, unmapped: {}, unleveraged: {}".format(
len(mapped), len(unmapped), len(unleveraged)))
if len(unmapped):
err = "Textures not mapped to json file"
print(err)
print(sorted(unmapped))
print("")
#return err
if len(unleveraged) > 20:
err = "Json file materials not found in obj test file, may need to update world"
print(err)
print(sorted(unleveraged))
# return err
if len(mapped) == 0:
return "No materials mapped"
elif len(mapped) < len(unmapped): # +len(unleveraged), too many esp. for Mineways
# not a very optimistic threshold, but better than none
return "More materials unmapped than mapped"
print("")
mc_count=0
jmc_count=0
mineways_count=0
# each element is [cannon_name, form], form is none if not matched
mapped = [get_mc_canonical_name(mat.name) for mat in bpy.data.materials]
# no matching canon name (warn)
mats_not_canon = [itm[0] for itm in mapped if itm[1] is None]
if mats_not_canon:
print("Non-canon material names found: ({})".format(len(mats_not_canon)))
print(mats_not_canon)
if len(mats_not_canon)>30: # arbitrary threshold
return "Too many materials found without canonical name ({})".format(
len(mats_not_canon))
else:
print("Confirmed - no non-canon images found")
# affirm the correct mappings
mats_no_packimage = [find_from_texturepack(itm[0]) for itm in mapped
if itm[1] is not None]
mats_no_packimage = [path for path in mats_no_packimage if path]
print("Mapped paths: "+str(len(mats_no_packimage)))
# could not resolve image from resource pack (warn) even though in mapping
mats_no_packimage = [itm[0] for itm in mapped
if itm[1] is not None and not find_from_texturepack(itm[0])]
print("No resource images found for mapped items: ({})".format(
len(mats_no_packimage)))
print("These would appear to have cannon mappings, but then fail on lookup")
if len(mats_no_packimage)>5: # known number up front, e.g. chests, stone_slab_side, stone_slab_top
return "Missing images for blocks specified in mcprep_data.json: "+",".join(mats_no_packimage)
# also test that there are not raw image names not in mapping list
# but that otherwise could be added to the mapping list as file exists
def import_jmc2obj(self):
"""Checks that material names in output obj match the mapping file"""
self._clear_scene()
self._import_jmc2obj_full()
res = self.import_materials_util("block_mapping_jmc")
return res
def import_mineways_separated(self):
"""Checks Mineways (single-image) material name mapping to mcprep_data"""
self._clear_scene()
self._import_mineways_separated()
#mcprep_data = self._get_mcprep_data()
res = self.import_materials_util("block_mapping_mineways")
return res
def import_mineways_combined(self):
"""Checks Mineways (multi-image) material name mapping to mcprep_data"""
self._clear_scene()
self._import_mineways_combined()
#mcprep_data = self._get_mcprep_data()
res = self.import_materials_util("block_mapping_mineways")
return res
def name_generalize(self):
"""Tests the outputs of the generalize function"""
from MCprep.util import nameGeneralize
test_sets = {
"ab":"ab",
"table.001":"table",
"table.100":"table",
"table001":"table001",
"fire_0":"fire_0",
# "fire_0_0001.png":"fire_0", not current behavior, but desired?
"fire_0_0001":"fire_0",
"fire_0_0001.001":"fire_0",
"fire_layer_1":"fire_layer_1",
"cartography_table_side1":"cartography_table_side1"
}
errors = []
for key in list(test_sets):
res = nameGeneralize(key)
if res != test_sets[key]:
errors.append("{} converts to {} and should be {}".format(
key, res, test_sets[key]))
else:
print("{}:{} passed".format(key, res))
if errors:
return "Generalize failed: "+", ".join(errors)
def canonical_name_no_none(self):
"""Ensure that MC canonical name never returns none"""
from MCprep.materials.generate import get_mc_canonical_name
from MCprep.util import materialsFromObj
self._clear_scene()
self._import_jmc2obj_full()
self._import_mineways_separated()
self._import_mineways_combined()
bpy.ops.object.select_all(action='SELECT')
mats = materialsFromObj(bpy.context.selected_objects)
canons = [[get_mc_canonical_name(mat.name)][0] for mat in mats]
if None in canons: # detect None response to canon input
return "Canon returned none value"
if '' in canons:
return "Canon returned empty str value"
# Ensure it never returns None
in_str, _ = get_mc_canonical_name('')
if in_str != '':
return "Empty str should return empty string, not" + str(in_str)
did_raise = False
try:
get_mc_canonical_name(None)
except:
did_raise = True
if not did_raise:
return "None input SHOULD raise error"
# TODO: patch conf.json_data["blocks"] used by addon if possible,
# if this is transformed into a true py unit test. This will help
# check against report (-MNGGQfGGTJRqoizVCer)
def canonical_test_mappings(self):
"""Test some specific mappings to ensure they return correctly."""
from MCprep.materials.generate import get_mc_canonical_name
misc = {
".emit": ".emit",
}
jmc_to_canon = {
"grass": "grass",
"mushroom_red": "red_mushroom",
# "slime": "slime_block", # KNOWN jmc, need to address
}
mineways_to_canon = {}
for map_type in [misc, jmc_to_canon, mineways_to_canon]:
for key, val in map_type.items():
res, mapped = get_mc_canonical_name(key)
if res == val:
continue
return "Wrong mapping: {} mapped to {} ({}), not {}".format(
key, res, mapped, val)
def meshswap_util(self, mat_name):
"""Run meshswap on the first object with found mat_name"""
from MCprep.util import select_set
if mat_name not in bpy.data.materials:
return "Not a material: "+mat_name
print("\nAttempt meshswap of "+mat_name)
mat = bpy.data.materials[mat_name]
obj = None
for ob in bpy.data.objects:
for slot in ob.material_slots:
if slot and slot.material == mat:
obj = ob
break
if obj:
break
if not obj:
return "Failed to find obj for "+mat_name
print("Found the object - "+obj.name)
bpy.ops.object.select_all(action='DESELECT')
select_set(obj, True)
res = bpy.ops.mcprep.meshswap()
if res != {'FINISHED'}:
return "Meshswap returned cancelled for "+mat_name
def meshswap_spawner(self):
"""Tests direct meshswap spawning"""
self._clear_scene()
scn_props = bpy.context.scene.mcprep_props
bpy.ops.mcprep.reload_meshswap()
if not scn_props.meshswap_list:
return "No meshswap assets loaded for spawning"
elif len(scn_props.meshswap_list)<15:
return "Too few meshswap assets available"
if bpy.app.version >= (2, 80):
# Add with make real = False
bpy.ops.mcprep.meshswap_spawner(block='Collection/banner', make_real=False)
# test doing two of the same one (first won't be cached, second will)
# Add one with make real = True
bpy.ops.mcprep.meshswap_spawner(block='Collection/fire', make_real=True)
if 'fire' not in bpy.data.collections:
return "Fire not in collections"
elif not bpy.context.selected_objects:
return "Added made-real meshswap objects not selected"
bpy.ops.mcprep.meshswap_spawner(block='Collection/fire', make_real=False)
if 'fire' not in bpy.data.collections:
return "Fire not in collections"
count_torch = sum([1 for itm in bpy.data.collections if 'fire' in itm.name])
if count_torch != 1:
return "Imported extra fire group, should have cached instead!"
# test that added item ends up in location location=(1,2,3)
loc = (1,2,3)
bpy.ops.mcprep.meshswap_spawner(block='Collection/fire', make_real=False, location=loc)
if not bpy.context.object:
return "Added meshswap object not added as active"
elif not bpy.context.selected_objects:
return "Added meshswap object not selected"
if bpy.context.object.location != Vector(loc):
return "Location not properly applied"
else:
# Add with make real = False
bpy.ops.mcprep.meshswap_spawner(block='Group/banner', make_real=False)
# test doing two of the same one (first won't be cached, second will)
# Add one with make real = True
bpy.ops.mcprep.meshswap_spawner(block='Group/fire', make_real=True)
if 'fire' not in bpy.data.groups:
return "Fire not in groups"
elif not bpy.context.selected_objects:
return "Added made-real meshswap objects not selected"
bpy.ops.mcprep.meshswap_spawner(block='Group/fire', make_real=False)
if 'fire' not in bpy.data.groups:
return "Fire not in groups"
count_torch = sum([1 for itm in bpy.data.groups if 'fire' in itm.name])
if count_torch != 1:
return "Imported extra fire group, should have cached instead!"
# test that added item ends up in location location=(1,2,3)
loc = (1,2,3)
bpy.ops.mcprep.meshswap_spawner(block='Group/fire', make_real=False, location=loc)
if not bpy.context.object:
return "Added meshswap object not added as active"
elif not bpy.context.selected_objects:
return "Added meshswap object not selected"
if bpy.context.object.location != Vector(loc):
return "Location not properly applied"
def meshswap_jmc2obj(self):
"""Tests jmc2obj meshswapping"""
self._clear_scene()
self._import_jmc2obj_full()
self._set_exporter('jmc2obj')
# known jmc2obj material names which we expect to be able to meshswap
test_materials = [
"torch",
"fire",
"lantern",
"cactus_side",
"vines", # plural
"enchant_table_top",
"redstone_torch_on",
"glowstone",
"redstone_lamp_on",
"pumpkin_front_lit",
"sugarcane",
"chest",
"largechest",
"sunflower_bottom",
"sapling_birch",
"white_tulip",
"sapling_oak",
"sapling_acacia",
"sapling_jungle",
"blue_orchid",
"allium",
]
errors = []
for mat_name in test_materials:
try:
res = self.meshswap_util(mat_name)
except Exception as err:
err = str(err)
if len(err)>15:
res = err[:15].replace("\n", "")
else:
res = err
if res:
errors.append(mat_name+":"+res)
if errors:
return "Meshswap failed: "+", ".join(errors)
def meshswap_mineways_separated(self):
"""Tests jmc2obj meshswapping"""
self._clear_scene()
self._import_mineways_separated()
self._set_exporter('Mineways')
# known Mineways (separated) material names expected for meshswap
test_materials = [
"grass",
"torch",