-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_zen.py
More file actions
1196 lines (1022 loc) · 47.9 KB
/
test_zen.py
File metadata and controls
1196 lines (1022 loc) · 47.9 KB
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
"""
'--------____________________________--------'
| | -ZEN- | |
|____| Reducing recompilation times. |____|
'----------------------------------'
Copyright 2019 TryExceptElse
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from unittest import TestCase
import os
from pathlib import Path
import shutil
import sys
import subprocess as sub
import tempfile
import typing as ty
import zen
ROOT = Path(os.path.dirname(__file__))
TEST_RESOURCES_PATH = Path(ROOT, 'test_resources')
SAMPLE_PROJECT_PATH = Path(TEST_RESOURCES_PATH, 'sample_project_1')
SAMPLE_BUILD_DIR = Path(SAMPLE_PROJECT_PATH, 'build')
FAKE_PROJECT_PATH = Path(
'/', 'home', 'user0', 'PycharmProjects', 'zen', 'test_resources',
'sample_project_1')
HELLO_H_PATH = Path(FAKE_PROJECT_PATH, 'hello', 'hello.h')
HELLO_CC_PATH = Path(FAKE_PROJECT_PATH, 'hello', 'hello.cc')
SAMPLE_H_PATH = Path(FAKE_PROJECT_PATH, 'sample.h')
SAMPLE_CC_PATH = Path(FAKE_PROJECT_PATH, 'sample.cc')
MAIN_CC_PATH = Path(FAKE_PROJECT_PATH, 'main.cc')
TEST_SOURCE_DIR_PATH = Path(TEST_RESOURCES_PATH, 'test_source_dir_1')
ALTERNATE_SAMPLE_H_PATH = Path(TEST_RESOURCES_PATH, 'alternate_sample.h')
ALTERNATE_HELLO_H_PATH = Path(TEST_RESOURCES_PATH, 'alternate_hello.h')
CHANGED_HELLO_CC_PATH = Path(TEST_RESOURCES_PATH, 'changed_hello.cc')
BROKEN_HELLO_CC_PATH = Path(TEST_RESOURCES_PATH, 'broken_hello.cc')
OUT_DIR_PATH = Path(TEST_RESOURCES_PATH, 'output')
CODE_SAMPLES_PATH = Path(TEST_RESOURCES_PATH, 'code_samples')
def get_output_content_dict():
d = {}
for name in os.listdir(OUT_DIR_PATH):
with Path(OUT_DIR_PATH, name).open('rb') as f:
d[name] = f.read()
return d
_out: ty.Dict[str, bytes] = get_output_content_dict()
class TestBuildDir(TestCase):
def tearDown(self):
zen.clear()
def test_build_dir_finds_targets(self):
build_dir = zen.BuildDir(SAMPLE_BUILD_DIR)
self.assertEqual(2, len(build_dir.targets))
self.assertIn('sample_target', build_dir.targets)
self.assertIn('hello', build_dir.targets)
def test_all_dependencies_are_found(self):
build_dir = zen.BuildDir(SAMPLE_BUILD_DIR)
self.assertIn(zen.SourceFile(HELLO_H_PATH), build_dir.sources)
self.assertIn(zen.SourceFile(HELLO_CC_PATH), build_dir.sources)
self.assertIn(zen.SourceFile(SAMPLE_H_PATH), build_dir.sources)
self.assertIn(zen.SourceFile(SAMPLE_CC_PATH), build_dir.sources)
self.assertIn(zen.SourceFile(MAIN_CC_PATH), build_dir.sources)
def test_meditation_prevents_doc_edit_from_causing_rebuild(self):
original_dir = os.curdir
try:
with tempfile.TemporaryDirectory() as temp_dir:
project_dir, build_dir = self.set_up_project_dir(temp_dir)
def make():
"""
Run Zen, followed by make, and then
remember the sources used for the build.
:return: bytes resulting from the make call.
"""
build_dir1 = zen.BuildDir(build_dir.absolute())
build_dir1.meditate()
out = sub.check_output(['make'])
build_dir2 = zen.BuildDir(build_dir.absolute())
build_dir2.remember()
return out
# First make - full project should build.
first_out = make()
self.assertEqual(_out['full_build'], first_out)
# Second make with no source changes:
# nothing should be rebuilt.
second_out = make()
self.assertEqual(_out['no_rebuild'], second_out)
# Change docs and whitespace in header.
shutil.copy(
ALTERNATE_SAMPLE_H_PATH.absolute(),
Path(project_dir, 'sample.h').absolute()
)
# Since no substantive changes have been made,
# no objects should need to be rebuilt.
last_out = make()
self.assertEqual(_out['no_rebuild'], last_out)
finally:
os.chdir(original_dir)
@staticmethod
def set_up_project_dir(temp_dir):
project_dir = Path(temp_dir, 'project')
# Copy sources to project dir
shutil.copytree(
TEST_SOURCE_DIR_PATH.absolute(),
project_dir.absolute()
)
build_dir = Path(project_dir, 'build')
build_dir.mkdir()
# CMake
os.chdir(build_dir.absolute())
sub.call(['cmake', '..'])
return project_dir, build_dir
@staticmethod
def make(build_path: Path):
"""
Run Zen, followed by make, and then
remember the sources used for the build.
:return: bytes resulting from the make call.
"""
zen_path = Path(ROOT, 'zen.py')
sub.check_call([
sys.executable,
str(zen_path),
'meditate',
str(build_path),
'--verbose'
])
out = sub.check_output(['make'])
sub.check_call([
sys.executable,
str(zen_path),
'remember',
str(build_path),
'--verbose'
])
return out
def test_meditation_prevents_doc_edit_from_causing_rebuild_cmd_line(self):
original_dir = os.curdir
try:
with tempfile.TemporaryDirectory() as temp_dir:
project_dir, build_dir = self.set_up_project_dir(temp_dir)
# First make - full project should build.
first_out = self.make(build_dir)
self.assertEqual(_out['full_build'], first_out)
# Second make with no source changes:
# nothing should be rebuilt.
second_out = self.make(build_dir)
self.assertEqual(_out['no_rebuild'], second_out)
# Change docs and whitespace in header.
shutil.copy(
ALTERNATE_SAMPLE_H_PATH.absolute(),
Path(project_dir, 'sample.h').absolute()
)
# Since no substantive changes have been made,
# no objects should need to be rebuilt.
last_out = self.make(build_dir)
self.assertEqual(_out['no_rebuild'], last_out)
finally:
os.chdir(original_dir)
def test_meditation_prevents_new_line_char_from_causing_rebuild(self):
original_dir = os.curdir
try:
with tempfile.TemporaryDirectory() as temp_dir:
project_dir, build_dir = self.set_up_project_dir(temp_dir)
# First make - full project should build.
first_out = self.make(build_dir)
self.assertEqual(_out['full_build'], first_out)
# Second make with no source changes:
# nothing should be rebuilt.
second_out = self.make(build_dir)
self.assertEqual(_out['no_rebuild'], second_out)
# Change docs and whitespace in header.
shutil.copy(
ALTERNATE_HELLO_H_PATH.absolute(),
Path(project_dir, 'hello', 'hello.h').absolute()
)
# Since no substantive changes have been made,
# no objects should need to be rebuilt.
last_out = self.make(build_dir)
self.assertEqual(_out['no_rebuild'], last_out)
finally:
os.chdir(original_dir)
def test_substantive_change_triggers_rebuilds(self):
original_dir = os.curdir
try:
with tempfile.TemporaryDirectory() as temp_dir:
project_dir, build_dir = self.set_up_project_dir(temp_dir)
# First make - full project should build.
first_out = self.make(build_dir)
self.assertEqual(_out['full_build'], first_out)
# Change function definition in hello.cc.
shutil.copy(
str(CHANGED_HELLO_CC_PATH),
str(Path(project_dir, 'hello', 'hello.cc'))
)
# Since no substantive changes have been made,
# no objects should need to be rebuilt.
last_out = self.make(build_dir)
self.assertEqual(_out['hello_rebuild'], last_out)
finally:
os.chdir(original_dir)
def test_build_failure_does_not_cause_break_follow_up_builds(self):
original_dir = os.curdir
try:
with tempfile.TemporaryDirectory() as temp_dir:
project_dir, build_dir = self.set_up_project_dir(temp_dir)
# First make - full project should build.
first_build_out = self.make(build_dir)
self.assertEqual(_out['full_build'], first_build_out)
# Check output of program
call_path = str(Path(build_dir, 'sample_target'))
first_program_out = sub.check_output([call_path])
self.assertEqual(_out['expected1'], first_program_out)
# Change function definition in hello.cc.
shutil.copy(
str(BROKEN_HELLO_CC_PATH),
str(Path(project_dir, 'hello', 'hello.cc'))
)
# Build with broken source
self.assertRaises(sub.CalledProcessError, self.make, build_dir)
# Fix source and rebuild
shutil.copy(
str(CHANGED_HELLO_CC_PATH),
str(Path(project_dir, 'hello', 'hello.cc'))
)
fixed_build_out = self.make(build_dir)
self.assertEqual(_out['hello_rebuild'], fixed_build_out)
# Re run program and check output.
fixed_program_out = sub.check_output([call_path])
self.assertEqual(_out['expected2'], fixed_program_out)
finally:
os.chdir(original_dir)
def get_output_from_change(
self,
change_source: ty.Callable[[str], None],
after_setup: ty.Callable[[str], None] = None,
) -> ty.Tuple[bytes, bytes, bytes]:
original_dir = os.curdir
try:
with tempfile.TemporaryDirectory() as temp_dir:
project_dir, build_dir = self.set_up_project_dir(temp_dir)
# Add original red herring class.
if after_setup:
after_setup(project_dir)
# First make - full project should build.
first_out = self.make(build_dir)
# Second make with no source changes:
# nothing should be rebuilt.
second_out = self.make(build_dir)
# Run change
change_source(project_dir)
# Since no substantive changes have been made,
# no objects should need to be rebuilt.
last_out = self.make(build_dir)
finally:
os.chdir(original_dir)
return first_out, second_out, last_out
def test_change_in_unused_member_function_does_not_cause_rebuild(self):
first_out, second_out, last_out = self.get_output_from_change(
# Add original red herring class.
after_setup=lambda project_dir: shutil.copy(
Path(TEST_RESOURCES_PATH, 'herring_sample1a.h'),
Path(project_dir, 'sample.h')
),
# Change red herring class.
# Since no substantive changes have been made,
# no objects should need to be rebuilt.
change_source=lambda project_dir: shutil.copy(
Path(TEST_RESOURCES_PATH, 'herring_sample1b.h'),
Path(project_dir, 'sample.h')
)
)
self.assertEqual(_out['full_build'], first_out)
self.assertEqual(_out['no_rebuild'], second_out)
self.assertEqual(_out['no_rebuild'], last_out)
def test_change_in_unused_function_does_not_cause_rebuild(self):
first_out, second_out, last_out = self.get_output_from_change(
# Add original red herring class.
after_setup=lambda project_dir: shutil.copy(
Path(TEST_RESOURCES_PATH, 'herring_sample2a.h'),
Path(project_dir, 'sample.h')
),
# Change red herring class.
# Since no substantive changes have been made,
# no objects should need to be rebuilt.
change_source=lambda project_dir: shutil.copy(
Path(TEST_RESOURCES_PATH, 'herring_sample2b.h'),
Path(project_dir, 'sample.h')
)
)
self.assertEqual(_out['full_build'], first_out)
self.assertEqual(_out['no_rebuild'], second_out)
self.assertEqual(_out['no_rebuild'], last_out)
def test_added_function_definition_does_not_cause_rebuild(self):
first_out, second_out, last_out = self.get_output_from_change(
# Add definition
change_source=lambda project_dir: shutil.copy(
Path(TEST_RESOURCES_PATH, 'herring_sample3.h'),
Path(project_dir, 'sample.h')
)
)
self.assertEqual(_out['full_build'], first_out)
self.assertEqual(_out['no_rebuild'], second_out)
self.assertEqual(_out['no_rebuild'], last_out)
def test_added_function_declaration_does_not_cause_rebuild(self):
first_out, second_out, last_out = self.get_output_from_change(
# Add definition
change_source=lambda project_dir: shutil.copy(
Path(TEST_RESOURCES_PATH, 'herring_sample4.h'),
Path(project_dir, 'sample.h')
)
)
self.assertEqual(_out['full_build'], first_out)
self.assertEqual(_out['no_rebuild'], second_out)
self.assertEqual(_out['no_rebuild'], last_out)
def test_changed_class_declaration_causes_rebuild(self):
first_out, second_out, last_out = self.get_output_from_change(
# Add definition
change_source=lambda project_dir: shutil.copy(
Path(TEST_RESOURCES_PATH, 'changed_sample.h'),
Path(project_dir, 'sample.h')
)
)
self.assertEqual(_out['full_build'], first_out)
self.assertEqual(_out['no_rebuild'], second_out)
self.assertEqual(_out['sample_rebuild'], last_out)
def test_change_in_unused_func_in_definition_file_causes_rebuild(self):
"""
Because functions in definition files may be linked to despite
not being used in the currently compiled object, any change to
a definition within a definition (.cc / .cpp) file should cause
a rebuild of objects dependant on that file.
"""
first_out, second_out, last_out = self.get_output_from_change(
# Add original red herring class.
after_setup=lambda project_dir: shutil.copy(
Path(TEST_RESOURCES_PATH, 'changed_hello1a.cc'),
Path(project_dir, 'hello', 'hello.cc')
),
# Change red herring class.
# Since no substantive changes have been made,
# no objects should need to be rebuilt.
change_source=lambda project_dir: shutil.copy(
Path(TEST_RESOURCES_PATH, 'changed_hello1b.cc'),
Path(project_dir, 'hello', 'hello.cc')
)
)
self.assertEqual(_out['full_build'], first_out)
self.assertEqual(_out['no_rebuild'], second_out)
self.assertEqual(_out['hello_rebuild'], last_out)
class TestTarget(TestCase):
def tearDown(self):
zen.clear()
def test_target_has_correct_objects(self):
target = zen.BuildDir(SAMPLE_BUILD_DIR).targets['sample_target']
self.assertEqual(2, len(target.objects))
def test_target_finds_library_dependencies(self):
build_dir = zen.BuildDir(SAMPLE_BUILD_DIR)
target = build_dir.targets['sample_target']
self.assertIn(build_dir.targets['hello'], target.lib_dependencies)
def test_target_finds_other_dependencies(self):
build_dir = zen.BuildDir(SAMPLE_BUILD_DIR)
target = build_dir.targets['sample_target']
build_file_path = Path(
build_dir.path, 'CMakeFiles', 'sample_target.dir', 'build.make')
link_file_path = Path(
build_dir.path, 'CMakeFiles', 'sample_target.dir', 'link.txt')
self.assertIn(build_file_path, target.other_dependencies)
self.assertIn(link_file_path, target.other_dependencies)
class TestSourceFile(TestCase):
def tearDown(self):
zen.clear()
def test_multiple_source_file_instantiations_produce_same_instance(self):
a = zen.SourceFile(HELLO_H_PATH)
b = zen.SourceFile(HELLO_H_PATH)
self.assertIs(a, b)
def test_duplicate_source_files_have_same_hash(self):
a = zen.SourceFile(HELLO_H_PATH)
b = zen.SourceFile(HELLO_H_PATH)
self.assertEqual(hash(a), hash(b))
def test_different_source_files_are_unique_instances(self):
definition_file = zen.SourceFile(HELLO_CC_PATH)
header_file = zen.SourceFile(HELLO_H_PATH)
self.assertIsNot(header_file, definition_file)
class TestSourceContent(TestCase):
def tearDown(self):
zen.clear()
def test_source_content_generates_lines(self):
content = zen.SourceContent('This file\nhas three\nlines.')
self.assertEqual(3, len(content.lines))
def test_uncomment_line_removes_block_comment_in_single_line(self):
content = zen.SourceContent(' this is a /* commented */ line')
content.strip_comments()
self.assertEqual(' this is a line', content.lines[0].uncommented)
def test_uncomment_line_removes_block_comment_in_multiple_lines(self):
content = zen.SourceContent(' this is a /* \ncommented\n */ line')
content.strip_comments()
self.assertEqual(' this is a \n', content.lines[0].uncommented)
self.assertEqual('\n', content.lines[1].uncommented)
self.assertEqual(' line', content.lines[2].uncommented)
def test_uncomment_line_removes_line_comment_in_single_line(self):
content = zen.SourceContent(' this is a line // with line comment')
content.strip_comments()
self.assertEqual(' this is a line ', content.lines[0].uncommented)
def test_removing_line_comment_does_not_remove_newline_char(self):
content = zen.SourceContent(' line // with newline char\n')
content.strip_comments()
self.assertEqual(' line \n', content.lines[0].uncommented)
def test_removing_block_comment_does_not_remove_newline_char(self):
content = zen.SourceContent(' this is a /*\ncommented\n*/ line')
content.strip_comments()
self.assertEqual(' this is a \n', content.lines[0].uncommented)
self.assertEqual('\n', content.lines[1].uncommented)
self.assertEqual(' line', content.lines[2].uncommented)
def test_stripped_line_with_content_is_correct(self):
content = zen.SourceContent(' this is a /* commented */ line\n')
content.strip_comments()
self.assertEqual('this is a line\n', content.lines[0].stripped)
def test_stripped_line_with_whitespace_is_correct(self):
content = zen.SourceContent(' \t\n')
content.strip_comments()
self.assertEqual('\n', content.lines[0].stripped)
def test_stripped_empty_line_is_correct(self):
content = zen.SourceContent('\n')
content.strip_comments()
self.assertEqual('\n', content.lines[0].stripped)
def test_stripped_hash_does_not_change_with_whitespace_line_number(self):
content1 = zen.SourceContent(' \n \nFoo')
content1.strip_comments()
content2 = zen.SourceContent(' \n \n\n \n\nFoo')
content2.strip_comments()
self.assertEqual(content1.stripped_hash, content2.stripped_hash)
def test_stripped_hash_changes_with_different_useful_content(self):
content1 = zen.SourceContent(' \n \nFoo')
content1.strip_comments()
content2 = zen.SourceContent(' \n \nBar')
content2.strip_comments()
self.assertNotEqual(content1.stripped_hash, content2.stripped_hash)
def test_preprocessor_directive_is_identified(self):
content1 = zen.SourceContent('// Preprocessor\n#include <string>\n\n')
self.assertIsInstance(
content1.component.sub_components[0],
zen.PreprocessorComponent
)
def test_correct_components_are_found(self):
with Path(TEST_RESOURCES_PATH, 'template_func.cc').open() as src_f:
# noinspection PyTypeChecker
content = zen.SourceContent(src_f)
components = content.component.sub_components
self.assertIsInstance(
components[0],
zen.PreprocessorComponent
)
self.assertIsInstance(
components[1],
zen.UsingStatement
)
self.assertIsInstance(
components[2],
zen.FunctionDefinition
)
self.assertEqual(1, len(components[2].construct_content))
self.assertIn('custom_max', components[2].construct_content)
self.assertIsInstance(
components[3],
zen.FunctionDefinition
)
self.assertEqual(1, len(components[3].construct_content))
self.assertIn('main', components[3].construct_content)
def test_correct_nested_components_are_found(self):
with ALTERNATE_SAMPLE_H_PATH.open() as src_f:
# noinspection PyTypeChecker
content = zen.SourceContent(src_f)
components = content.component.sub_components
self.assertEqual(2, len(components))
self.assertIsInstance(components[0], zen.PreprocessorComponent)
self.assertIsInstance(components[1], zen.NamespaceComponent)
ns_components = components[1].sub_components
self.assertEqual(1, len(ns_components))
self.assertIsInstance(ns_components[0], zen.CppClassDefinition)
self.assertIn('Foo', ns_components[0].construct_content)
# noinspection PyUnresolvedReferences
class_components = ns_components[0].inner_block.sub_components
self.assertEqual(5, len(class_components))
self.assertIsInstance(
class_components[1],
zen.MemberFunctionDefinition
)
self.assertIn('Foo', class_components[1].construct_content)
self.assertIsInstance(
class_components[2],
zen.MemberFunctionDeclaration
)
self.assertIn('Print', class_components[2].construct_content)
self.assertIsInstance(
class_components[4],
zen.MiscStatement
)
class TestSourcePos(TestCase):
def test_position_can_be_added_to(self):
content = zen.SourceContent('This file\nhas three\nlines.')
chunk = zen.Chunk(content)
a = chunk.start
b = a + 10
c = a + 25
self.assertEqual('T', chunk[a])
self.assertEqual('h', chunk[b])
self.assertEqual('.', chunk[c])
def test_addition_does_not_modify_operands(self):
content = zen.SourceContent('This file\nhas three\nlines.')
chunk = zen.Chunk(content)
a = chunk.start
initial_line_i = a.line_i
initial_col_i = a.col_i
a + 10
self.assertEqual(initial_line_i, a.line_i)
self.assertEqual(initial_col_i, a.col_i)
def test_position_can_be_added_to_to_get_end_pos(self):
content = zen.SourceContent('This file\nhas three\nlines.')
chunk = zen.Chunk(content)
a = chunk.start
b = a + 26
self.assertEqual(b, chunk.end)
def test_position_can_be_added_to_to_get_end_pos_on_same_line(self):
content = zen.SourceContent('This file\nhas three\nlines.')
chunk = zen.Chunk(content)
a = chunk.start + 22
b = a + 4
self.assertEqual(b, chunk.end)
def test_negative_number_can_be_added(self):
content = zen.SourceContent('This file\nhas three\nlines.')
chunk = zen.Chunk(content)
a = chunk.end + -1
b = chunk.end + -16
c = chunk.end + -26
self.assertEqual('T', chunk[c])
self.assertEqual('h', chunk[b])
self.assertEqual('.', chunk[a])
def test_position_addition_throws_value_error_if_x_too_large(self):
content = zen.SourceContent('This file\nhas three\nlines.')
chunk = zen.Chunk(content)
self.assertRaises(ValueError, lambda: chunk.start + 27)
def test_position_can_be_subtracted_from(self):
content = zen.SourceContent('This file\nhas three\nlines.')
chunk = zen.Chunk(content)
a = chunk.end - 1
b = chunk.end - 16
c = chunk.end - 26
self.assertEqual('T', chunk[c])
self.assertEqual('h', chunk[b])
self.assertEqual('.', chunk[a])
def test_negative_number_can_be_subtracted(self):
content = zen.SourceContent('This file\nhas three\nlines.')
chunk = zen.Chunk(content)
a = chunk.start
b = a - -10
c = a - -25
self.assertEqual('T', chunk[a])
self.assertEqual('h', chunk[b])
self.assertEqual('.', chunk[c])
def test_position_subtraction_throws_value_error_if_x_too_large(self):
content = zen.SourceContent('This file\nhas three\nlines.')
chunk = zen.Chunk(content)
self.assertRaises(ValueError, lambda: chunk.end - 27)
def test_next_line_can_be_accessed(self):
content = zen.SourceContent('This file\nhas three\nlines.')
chunk = zen.Chunk(content)
a = zen.SourcePos(content, 0, 4, zen.SourceForm.STRIPPED)
b = a.next_line_pos
c = b.next_line_pos
self.assertEqual('h', chunk[b])
self.assertEqual('l', chunk[c])
def test_positions_with_same_indices_are_equal(self):
content = zen.SourceContent('This file\nhas three\nlines.')
a = zen.SourcePos(content, 0, 4, zen.SourceForm.STRIPPED)
b = zen.SourcePos(content, 0, 4, zen.SourceForm.STRIPPED)
self.assertEqual(a, b)
def test_positions_with_same_indices_have_same_hash(self):
content = zen.SourceContent('This file\nhas three\nlines.')
a = zen.SourcePos(content, 0, 4, zen.SourceForm.STRIPPED)
b = zen.SourcePos(content, 0, 4, zen.SourceForm.STRIPPED)
self.assertEqual(hash(a), hash(b))
def test_positions_with_different_indices_are_not_equal(self):
content = zen.SourceContent('This file\nhas three\nlines.')
a = zen.SourcePos(content, 0, 4, zen.SourceForm.STRIPPED)
b = zen.SourcePos(content, 1, 4, zen.SourceForm.STRIPPED)
self.assertNotEqual(a, b)
class TestChunk(TestCase):
def test_chunk_created_from_content_has_correct_length(self):
content = zen.SourceContent('\n\n\n\n\n\nclass Foo')
self.assertEqual(15, len(zen.Chunk(content)))
def test_chunk_created_from_content_same_string(self):
s = '\n\n\n\n\n\nclass Foo'
content = zen.SourceContent(s)
self.assertEqual(s, str(zen.Chunk(content)))
def test_chunk_with_invalid_start_and_end_order_raises_error(self):
content = zen.SourceContent('This file\nhas three\nlines.')
# Passing an end line preceding the start line should
# cause an error.
start_pos1 = zen.SourcePos(content, 1, 2, zen.SourceForm.STRIPPED)
end_pos1 = zen.SourcePos(content, 0, 5, zen.SourceForm.STRIPPED)
self.assertRaises(ValueError, zen.Chunk, content, start_pos1, end_pos1)
# Passing an end col preceding the start col should
# cause an error if chunk starts and ends on the same line.
start_pos2 = zen.SourcePos(content, 0, 5, zen.SourceForm.STRIPPED)
end_pos2 = zen.SourcePos(content, 0, 2, zen.SourceForm.STRIPPED)
self.assertRaises(ValueError, zen.Chunk, content, start_pos2, end_pos2)
def test_chunk_index_accessor_works_correctly(self):
content = zen.SourceContent('This file\nhas three\nlines.')
chunk = zen.Chunk(content)
self.assertEqual('T', chunk[0])
self.assertEqual('h', chunk[10])
self.assertEqual('.', chunk[-1])
def test_chunk_index_accessor_works_when_chunk_starts_mid_line(self):
content = zen.SourceContent('This file\nhas three\nlines.')
start_pos = zen.SourcePos(content, 0, 5, zen.SourceForm.STRIPPED)
chunk = zen.Chunk(content, start=start_pos)
assert chunk[5] == 'h'
assert chunk[15] == 'l'
def test_source_pos_can_be_passed_to_getitem(self):
content = zen.SourceContent('This file\nhas three\nlines.')
chunk = zen.Chunk(content)
self.assertEqual('T', chunk[chunk.pos(0, 0)])
self.assertEqual('h', chunk[chunk.pos(1, 0)])
self.assertEqual('.', chunk[chunk.pos(-1, -1)])
def test_source_pos_can_be_sliced_with_positions(self):
content = zen.SourceContent('This file\nhas three\nlines.')
chunk = zen.Chunk(content)
a = chunk[:chunk.pos(1, 3)]
b = a[chunk.pos(1):]
self.assertEqual('This file\nhas three\nlines.', str(chunk))
self.assertEqual('This file\nhas', str(a))
self.assertEqual('has', str(b))
def test_chunk_can_be_turned_into_string(self):
content = zen.SourceContent('This file\nhas three\nlines.')
chunk = zen.Chunk(content)
self.assertEqual('This file\nhas three\nlines.', str(chunk))
def test_chunk_can_be_turned_into_string2(self):
content = zen.SourceContent('This file\nhas three\nlines.')
start = zen.SourcePos(content, 1, 0, zen.SourceForm.STRIPPED)
end = zen.SourcePos(content, 1, 3, zen.SourceForm.STRIPPED)
chunk = zen.Chunk(content, start, end)
self.assertEqual('has', str(chunk))
def test_chunk_chars_can_be_iterated_over(self):
content = zen.SourceContent('This file\nhas three\nlines.')
chunk = zen.Chunk(content)
chars = [c for c in chunk]
self.assertEqual(len('This file\nhas three\nlines.'), len(chars))
self.assertEqual('T', chars[0])
self.assertEqual('h', chars[10])
self.assertEqual('.', chars[-1])
def test_chunk_chars_can_be_iterated_over2(self):
content = zen.SourceContent('This file\nhas three\nlines.')
start = zen.SourcePos(content, 1, 0, zen.SourceForm.STRIPPED)
end = zen.SourcePos(content, 1, 3, zen.SourceForm.STRIPPED)
chunk = zen.Chunk(content, start, end)
self.assertEqual('has', ''.join(char for char in chunk))
def test_chunk_can_be_initialized_with_end_of_line_position(self):
content = zen.SourceContent('This file\nhas three\nlines.')
start_pos = zen.SourcePos(
content, 0, len('This file\n'), zen.SourceForm.STRIPPED)
chunk = zen.Chunk(content, start_pos)
self.assertEqual('has three\nlines.', str(chunk))
def test_chunk_may_be_default_tokenized(self):
content = zen.SourceContent('This file\nhas three\nlines.\nfoo123')
chunk = zen.Chunk(content)
self.assertEqual(
['This', 'file', 'has', 'three', 'lines', 'foo123'], # No period.
chunk.tokenize()
)
def test_simple_bracket_pair_can_be_found(self):
content = zen.SourceContent('Some bracket {\nfoo;\n}\n')
chunk = zen.Chunk(content)
first_bracket_pos = chunk.pos(0, -2)
end_pos = chunk.find_pair(first_bracket_pos)
self.assertEqual(2, end_pos.line_i)
self.assertEqual(0, end_pos.col_i)
def test_nested_bracket_pair_can_be_found(self):
content = zen.SourceContent('{Some bracket {\n{foo};\n}\n}')
chunk = zen.Chunk(content)
first_bracket_pos = chunk.pos(0, -2)
end_pos = chunk.find_pair(first_bracket_pos)
self.assertEqual(2, end_pos.line_i)
self.assertEqual(0, end_pos.col_i)
def test_bracket_pair_can_be_found_when_quoted_brackets_are_used(self):
content = zen.SourceContent('{foo {\n{foo("bracket: }")};\n}\n}')
chunk = zen.Chunk(content)
first_bracket_pos = chunk.pos(0, -2)
end_pos = chunk.find_pair(first_bracket_pos)
self.assertEqual(2, end_pos.line_i)
self.assertEqual(0, end_pos.col_i)
def test_bracket_pair_can_be_found_when_bracket_char_is_used(self):
content = zen.SourceContent("{foo {\n{foo('}')};\n}\n}")
chunk = zen.Chunk(content)
first_bracket_pos = chunk.pos(0, -2)
end_pos = chunk.find_pair(first_bracket_pos)
self.assertEqual(2, end_pos.line_i)
self.assertEqual(0, end_pos.col_i)
def test_quote_end_can_be_found(self):
content = zen.SourceContent('foo("some [string]\\" argument")')
chunk = zen.Chunk(content)
first_quote_pos = chunk.pos(0, 4)
end_pos = chunk.find_quote_end(first_quote_pos)
self.assertEqual(0, end_pos.line_i)
self.assertEqual(29, end_pos.col_i)
def test_line_can_be_retrieved_from_pos(self):
content = zen.SourceContent('{Some bracket {\n{foo};\n}\n}')
chunk = zen.Chunk(content)
pos = chunk.pos(1, 2)
self.assertEqual(1, chunk.line(pos).index)
def test_chunk_can_strip_whitespace_lines(self):
content = zen.SourceContent('\n\n\nfoo("some arg")\n\n')
chunk = zen.Chunk(content)
stripped = chunk.strip()
self.assertEqual(3, stripped.start.line_i)
self.assertEqual(0, stripped.start.col_i)
self.assertEqual(3, stripped.end.line_i)
self.assertEqual(15, stripped.end.col_i)
def test_chunk_does_not_strip_non_whitespace_chars(self):
content = zen.SourceContent('\n\n\nfoo("some arg")')
chunk = zen.Chunk(content)
stripped = chunk.strip()
self.assertEqual(3, stripped.start.line_i)
self.assertEqual(0, stripped.start.col_i)
self.assertEqual(3, stripped.end.line_i)
self.assertEqual(15, stripped.end.col_i)
self.assertEqual('foo("some arg")', str(stripped))
def test_chunk_can_be_stripped_when_trailing_spaces_are_present(self):
content = zen.SourceContent('\n\n\nfoo("some arg") \n\n')
chunk = zen.Chunk(content)
stripped = chunk.strip()
self.assertEqual(3, stripped.start.line_i)
self.assertEqual(0, stripped.start.col_i)
self.assertEqual(3, stripped.end.line_i)
self.assertEqual(15, stripped.end.col_i)
def test_chunk_hash_is_same_for_chunks_with_same_stripped_content(self):
content_a = zen.SourceContent('This file\nhas three\nlines.')
chunk_a = zen.Chunk(content_a)
hash_a = chunk_a.content_hash
content_b = zen.SourceContent('This file\n has three\nlines.')
chunk_b = zen.Chunk(content_b)
hash_b = chunk_b.content_hash
self.assertEqual(hash_a, hash_b)
def test_chunk_hash_is_same_for_chunks_with_same_stripped_content2(self):
content_a = zen.SourceContent('This file\nhas three\nlines.')
chunk_a = zen.Chunk(content_a)
hash_a = chunk_a[chunk_a.pos(1, 0):].content_hash
content_b = zen.SourceContent('file\n has three\nlines.')
chunk_b = zen.Chunk(content_b)
hash_b = chunk_b[chunk_b.pos(1, 0):].content_hash
self.assertEqual(hash_a, hash_b)
def test_chunk_hash_differs_if_content_does(self):
content_a = zen.SourceContent('This file\nhas three\nlines.')
chunk_a = zen.Chunk(content_a)
hash_a = chunk_a.content_hash
content_b = zen.SourceContent('file\n has three\nlines.')
chunk_b = zen.Chunk(content_b)
hash_b = chunk_b.content_hash
self.assertNotEqual(hash_a, hash_b)
class TestCppClassForwardDeclaration(TestCase):
def test_name_is_correct(self):
content = zen.SourceContent(
'\n\nclass __some_attr Foo;'
)
declaration = zen.CppClassForwardDeclaration(content.component.chunk)
self.assertEqual('Foo', declaration.name)
class TestCppClassDefinition(TestCase):
def get_class_def(self) -> zen.CppClassDefinition:
with Path(CODE_SAMPLES_PATH, 'sample_class').open() as src_f:
# noinspection PyTypeChecker
content = zen.SourceContent(src_f)
definition = content.component.sub_components[0]
self.assertIsInstance(definition, zen.CppClassDefinition)
return definition
def test_class_produces_correctly_named_constructs(self):
definition = self.get_class_def()
self.assertEqual(2, len(definition.construct_content))
self.assertIn('Foo', definition.construct_content)
self.assertIn('Print', definition.construct_content)
def test_class_members_are_not_in_sub_components(self):
definition = self.get_class_def()
self.assertEqual([], definition.sub_components)
def test_correct_number_of_member_components_are_found(self):
definition = self.get_class_def()
self.assertEqual(5, len(definition.member_components))
def test_class_labels_are_found(self):
definition = self.get_class_def()
components = definition.member_components
self.assertIsInstance(components[0], zen.Label)
self.assertIsInstance(components[3], zen.Label)
def test_class_constructor_has_correct_type(self):
definition = self.get_class_def()
components = definition.member_components
self.assertIsInstance(components[1], zen.MemberFunctionDefinition)
def test_member_function_declaration_has_correct_type(self):
definition = self.get_class_def()
components = definition.member_components
self.assertIsInstance(components[2], zen.MemberFunctionDeclaration)
def test_member_variable_declaration_has_correct_type(self):
definition = self.get_class_def()
components = definition.member_components
self.assertIsInstance(components[4], zen.MiscStatement)
def test_tokens_are_correct(self):
definition = self.get_class_def()
self.assertEqual(['class', 'Foo'], definition.tokens)
def test_exposed_content_only_includes_prefix(self):
definition = self.get_class_def()
self.assertEqual(1, len(definition.exposed_content))
self.assertEqual('class Foo', str(definition.exposed_content[0]))
def test_used_constructs(self):
sample_constructs = {
'std': zen.Construct('foo_1'),
'vector': zen.Construct('foo_2'),
'Foo': zen.Construct('stuff'),
'Fizz': zen.Construct('should be unused1'),
'Buzz': zen.Construct('should be unused2'),
}
definition = self.get_class_def()
used_constructs = definition.used_constructs(sample_constructs)
self.assertEqual({'std', 'vector'}, set(used_constructs.keys()))
class TestFunctionDeclaration(TestCase):
def test_declaration_has_no_external_content(self):
"""
Since function declarations cannot change the operation of a
program without being used, function declarations should have
no exposed content.
The code of the function declaration matters only if the
function is used.
"""
content = zen.SourceContent(
'\n\nstd::string RedHerring();\n'
)
definition = zen.FunctionDeclaration(content.component.chunk)
self.assertEqual([], definition.exposed_content)
class TestFunctionDefinition(TestCase):
def test_tokens_are_correct(self):
content = zen.SourceContent(
'\n\ninline std::string RedHerring() { return "Hello"; }\n'
)
definition = zen.FunctionDefinition(content.component.chunk)
self.assertEqual(
['inline', 'std', 'string', 'RedHerring'],
definition.tokens
)
def test_tags_are_found_in_multi_line_function(self):
content = zen.SourceContent(
'void Sample() const {\n'
' // ZEN(note1)\n'
' foo(); // ZEN(note2)\n'
'}'
)
definition = zen.FunctionDefinition(content.component.chunk)