Skip to content

Commit cf5081c

Browse files
committed
Add test mecanism for FileManager (TreeScript)
1 parent bd0d879 commit cf5081c

File tree

5 files changed

+410
-0
lines changed

5 files changed

+410
-0
lines changed

test/test_file_manager.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import unittest
2+
from tree_generator import TreeScript
3+
4+
class FileManagerTest(unittest.TestCase):
5+
maxDiff = None
6+
7+
def run_script(self, script_file):
8+
"""
9+
Perform test from a single tree script.
10+
"""
11+
script_manager = TreeScript(script_file)
12+
try:
13+
result_tree, expected_tree = script_manager.launch()
14+
finally:
15+
script_manager.clean()
16+
self.assertEqual(str(result_tree), str(expected_tree))
17+
18+
def test_first(self):
19+
self.run_script("test/tree_script/test.txt")

test/tree_generator/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .tree_script import TreeScript

test/tree_generator/file_utils.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import os
2+
import shutil
3+
import difflib
4+
from pathlib import Path
5+
6+
class Workdir:
7+
"""
8+
Context manager to switch working directory safely.
9+
"""
10+
def __init__(self, directory):
11+
self.destination_dir = directory
12+
13+
if not os.path.exists(directory):
14+
Path(directory).mkdir()
15+
16+
def __enter__(self):
17+
self._old_pwd = os.getcwd()
18+
os.chdir(self.destination_dir)
19+
20+
def __exit__(self, *args, **kwargs):
21+
os.chdir(self._old_pwd)
22+
23+
class TaggedFile:
24+
"""
25+
Represent a single file with his corresponding
26+
subfolder level.
27+
28+
Used to facitilate tagging of tree/folder exploration.
29+
"""
30+
def __init__(self, filename, level, isdir=None):
31+
if isdir is None:
32+
isdir = filename.endswith("/")
33+
34+
self.isdir = isdir
35+
self.level = level
36+
self.filename = self.clean_filename(filename)
37+
38+
@staticmethod
39+
def clean_filename(filename):
40+
if filename.endswith("/"):
41+
filename = filename[:-1]
42+
return filename.replace('|-', "").strip()
43+
44+
class Directory:
45+
"""
46+
Represent a single directory, with all availables files
47+
and directories inside the given folder.
48+
49+
Understand which folder is a directory, and will store
50+
directory childrens, subfolder also use Directory.
51+
"""
52+
def __init__(self, name, potential_files, level=-1):
53+
self.name = name
54+
self.level = level
55+
self.potential_files = potential_files
56+
57+
self.files = []
58+
self.directories = []
59+
60+
self._retrieve_directory_files()
61+
self._sort()
62+
63+
64+
def generate(self):
65+
"""
66+
Create each file and directory expected by
67+
the given template.
68+
"""
69+
with Workdir(self.name):
70+
for directory in self.directories:
71+
directory.generate()
72+
73+
for filename in self.files:
74+
Path(filename).touch()
75+
76+
def delete(self):
77+
"""
78+
Delete Directory folder
79+
"""
80+
shutil.rmtree(self.name)
81+
82+
def diff(self, other):
83+
"""
84+
Compare two different document, an return diff output.
85+
86+
Use the string representation of Directory to perform
87+
comparaison.
88+
With alphabetical sort and identical files/folder,
89+
it must be the same.
90+
"""
91+
self_repr = str(self).split("\n")
92+
other_repr = str(other).split("\n")
93+
res_diff = difflib.ndiff(self_repr, other_repr)
94+
diff_ouput = "+ : Program output\n- : Reference output\n\n"
95+
return diff_ouput + "\n".join(res_diff)
96+
97+
98+
def __eq__(self, other):
99+
return str(self) == str(other)
100+
101+
def __str__(self):
102+
return self.__repr__()
103+
104+
def __repr__(self):
105+
"""
106+
Print directory as a tree. Create back equivalent
107+
of the template script file.
108+
109+
To be ordered, display sub folder, followed by regular files.
110+
All directory and files are sorted in alphabetical order.
111+
"""
112+
# Convert back Directory object to his string representation
113+
directory_repr = ""
114+
115+
#Format Directory name
116+
if self.name and self.level >= 0:
117+
#add indentation and '|-' on left side
118+
directory_repr = " " * self.level
119+
directory_repr += "|- " if self.level else ""
120+
121+
directory_repr += self.name + "/\n"
122+
123+
#Recursive formatting of sub folders
124+
for sub_directory in self.directories:
125+
directory_repr += repr(sub_directory)
126+
127+
#Format all regular files
128+
for filename in self.files:
129+
#add indentation and '|-' on left side
130+
directory_repr += " " * (self.level + 1)
131+
directory_repr += "|- " if self.level > 0 else ""
132+
133+
directory_repr += filename + "\n"
134+
135+
return directory_repr
136+
137+
def _sort(self):
138+
"""
139+
Order alphabetically files and directories.
140+
"""
141+
self.directories.sort(key=lambda directory : directory.name)
142+
self.files.sort()
143+
144+
(directory._sort() for directory in self.directories)
145+
146+
def _retrieve_directory_files(self):
147+
"""
148+
Indentify files and directories inside the current
149+
directory.
150+
"""
151+
for index, tagged_file in enumerate(self.potential_files):
152+
#Element contained by the directory
153+
if tagged_file.level == self.level + 1:
154+
if tagged_file.isdir:
155+
new_dir = Directory(
156+
tagged_file.filename,
157+
self.potential_files[index + 1:],
158+
self.level + 1,
159+
)
160+
self.directories.append(new_dir)
161+
162+
else:
163+
self.files.append(tagged_file.filename)
164+
165+
## Stop iteration if actual file in template list
166+
## is a parent folder
167+
elif tagged_file.level == self.level:
168+
break

test/tree_generator/tree_script.py

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
#!/usr/bin/env python3
2+
3+
from pathlib import Path
4+
from bss_converter.file_manager import FileManager
5+
from .file_utils import TaggedFile, Directory, Workdir
6+
import shutil
7+
import difflib
8+
import os
9+
import re
10+
11+
12+
class ScriptFormatError(Exception):
13+
pass
14+
15+
class TreeScript:
16+
"""
17+
Parse template of tree, generate file
18+
and folders of the template, to compare moved files moved by
19+
FileManager class.
20+
"""
21+
DJANGO_DIR = os.path.realpath("tmp_django")
22+
BSS_DIR = "tmp_bss"
23+
def __init__(self, script_file):
24+
# Fix folder name at the top of template
25+
# represent Boostrap Studio export folder or
26+
# a django project folder.
27+
script, reference = self.from_script(script_file)
28+
self.script = Directory(self.BSS_DIR, script)
29+
self.reference = Directory('django', reference)
30+
31+
def launch(self):
32+
#Create fake bss and django project
33+
self.script.generate()
34+
self.emulate_django_project()
35+
36+
os.environ["DJANGO_PROJECT"] = self.DJANGO_DIR
37+
38+
with Workdir(self.BSS_DIR):
39+
FileManager()
40+
result_folders = self.from_directory(self.DJANGO_DIR)
41+
result_tree = Directory("django_result", result_folders)
42+
43+
return self.reference, result_tree
44+
45+
def clean(self):
46+
shutil.rmtree(self.BSS_DIR, ignore_errors=True)
47+
shutil.rmtree(self.DJANGO_DIR, ignore_errors=True)
48+
49+
@staticmethod
50+
def file_indent(filename):
51+
"""
52+
Find space size of left indentation.
53+
"""
54+
return len(filename) - len(filename.lstrip())
55+
56+
def tag_folder_lvl(self, list_file):
57+
"""
58+
Tag to each line a folder lvl, to understand subfolders
59+
are organized.
60+
61+
Return list of line with tag number:
62+
ex:
63+
[
64+
(0, "assets/"),
65+
(1, "img/"),
66+
(2, "logo.png"),
67+
(1, "js/"),
68+
...
69+
]
70+
71+
Is equivalent to:
72+
73+
assets/
74+
|- img/
75+
|- logo.png
76+
|- js/
77+
"""
78+
tagged_lines = []
79+
real_folder_lvl = 0
80+
81+
#Match a specific spacing with a folder level.
82+
#Expect having file from a same level to have
83+
#exact same number of spaces/tabulations
84+
available_levels = {
85+
0:real_folder_lvl,
86+
}
87+
88+
# Go through each line, find left space in the line,
89+
# and use this spaces to tag a folder level
90+
# using `available_levels`
91+
for filename in list_file:
92+
file_indent = self.file_indent(filename)
93+
if file_indent not in available_levels:
94+
real_folder_lvl += 1
95+
available_levels[file_indent] = real_folder_lvl
96+
97+
current_folder_lvl = available_levels[file_indent]
98+
99+
file_info = TaggedFile(filename, current_folder_lvl)
100+
tagged_lines.append(file_info)
101+
102+
return tagged_lines
103+
104+
def from_script(self, script_file):
105+
"""
106+
Read strip and clean unexpected content.
107+
108+
Clean apply:
109+
- split test and result tree
110+
- right strip every line
111+
"""
112+
with open(script_file) as script_stream:
113+
self.apps = self.find_apps(script_stream.readline())
114+
script_content = script_stream.read()
115+
116+
# Split test tree from a reference tree
117+
# Delimited by EQUAL, delimited with some equal sign.
118+
#
119+
# Example (see tests):
120+
# =================== EQUAL ====================
121+
test_script, result_script = re.split("=+\s*EQUAL\s*=+",
122+
script_content,
123+
flags=re.IGNORECASE)
124+
125+
def clean_script(self, script):
126+
line_split_script = script.strip().split("\n")
127+
rstripped_lines = map(str.rstrip, line_split_script)
128+
return self.tag_folder_lvl(rstripped_lines)
129+
130+
test_script = clean_script(self, test_script)
131+
result_script = clean_script(self, result_script)
132+
133+
return test_script, result_script
134+
135+
def find_apps(self, app_config):
136+
"""
137+
Retrieve list of django application, in the fist
138+
line of a script.
139+
"""
140+
if not app_config.startswith("apps="):
141+
raise ScriptFormatError("Please specify script applications,"
142+
" using 'apps=' at the first line.")
143+
apps_string = app_config.split("=")[1].strip()
144+
apps = apps_string.split(",")
145+
return list(filter("".__ne__, apps))
146+
147+
def emulate_django_project(self):
148+
"""
149+
Create folder structure of a django project, to
150+
emulate file migration.
151+
152+
Create single folder for each specified apps.
153+
154+
App configuration is given in the first line of a test
155+
script, with a format like:
156+
157+
apps=django-app[settings],app1,app2[,...]
158+
159+
- [settings] is used to generates a settings.py file in app.
160+
"""
161+
with Workdir(self.DJANGO_DIR):
162+
for application in self.apps:
163+
164+
#Check which app represent the settings module
165+
is_setting = False
166+
if "[settings]" in application:
167+
application = application.replace(
168+
"[settings]", ""
169+
)
170+
is_setting = True
171+
172+
#Create application folder in any case
173+
Path(application).mkdir()
174+
175+
# Add settings.py in expected application
176+
if is_setting:
177+
settings_file = os.path.join(
178+
application, "settings.py")
179+
Path(settings_file).touch()
180+
181+
def from_directory(self, directory_name, level=0):
182+
"""
183+
Create a template from an existing directory.
184+
Generate tagged list with subfolder level,
185+
equivalent to `TreeScript.tag_folder_lvl`
186+
"""
187+
list_files = []
188+
for filename in os.listdir(directory_name):
189+
relativ_file = os.path.join(directory_name, filename)
190+
is_dir = os.path.isdir(relativ_file)
191+
192+
list_files.append(TaggedFile(filename, level, is_dir))
193+
194+
if is_dir:
195+
sub_files = self.from_directory(relativ_file,
196+
level + 1)
197+
list_files.extend(sub_files)
198+
return list_files

0 commit comments

Comments
 (0)