Skip to content

Commit 1ed6f77

Browse files
committed
test: Adds in inital testing framework, coverage, pep-8 linting, and BDD-style testing
fix #12, fix #33
1 parent 8455de8 commit 1ed6f77

File tree

8 files changed

+544
-23
lines changed

8 files changed

+544
-23
lines changed

.travis.yml

+3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ before_script:
2727
script:
2828
# Runs through the test suite
2929
- bin/lint
30+
- pip install -e .
31+
- pytest --pep8 --cov=pydotfiles tests/ --cov-fail-under=10
3032

3133
after_success:
3234
- bin/generate-release
@@ -37,6 +39,7 @@ deploy:
3739
user: "JasonYao"
3840
password:
3941
secure: 0g/tLWbYAb9JrqKds8qqFuYk55o9su4aSr27iKXEDVaCsiFWLKcanB3tgCE/TkM/M6+2CCJXWcxRqpwoUkGgkBKOus1nNDYymEo7TTpmGWMx93oJAU6XU1YK+z46EX8QbkmVIAA33qe7NeB9liDfXE0m1Od7t58kqbIjXxla37DRIxpdEQR/r8yhu8y6AydbxS5rL0qDASb3LunB/sJvfaPRfyPYG0tf/IYazGDFR+v2BPpSXYLOtToGop+3UtChH71K4v8F8K9xoVLGab856DbolUXgNN4jn8HdzRwShgLqNpoWY8lPLc6rY6ANf217zWvGJCcEudyCYAGasFFG22V4Almqsca5ltHDFH6nBqDlVtRhaNkeyU+6gh7jTW2h0HPOwQivh/oPz334RNft7RIoI+ydgWVtfoU9UmqNBmtxDCII+mWKRcBND68mXppCOIiBYbPJQmb9bT7k5oLvAnGqd9zuH30ZhGqHkxTPAmd7tZx791v46yT0bAXeS7jS7Tpg1k+TS/wfo6nrVz7+t1EI5HOkD9ptZiWGVORD+V7JPO53efazaVvG6H3U82QPTWVNg9SNaFdEUvUWBzPQlXKVvC5tio6hQc+obUYnxudXZJb7Sp5yVGouJzwskFn/3T9gxTMn6vtQZ8rc4gdqpyNBi65oudT5cIL9CVQ81I0=
42+
# We want both source code and binary releases
4043
distributions: "sdist bdist_wheel"
4144

4245
# We want to use the existing build

bin/install_dependencies

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ set -e
77
# Installs required dependencies
88
##
99

10-
# Linting dependency (should have been in dev but it doesn't work)
11-
pip3 install autopep8
10+
# Linting/testing dependencies (should have been in dev but it doesn't work)
11+
pip3 install autopep8 pytest pytest-pep8 pytest-cov
1212

1313
# Automatic semantic release plugin
1414
npm install -g semantic-release \

pydotfiles/api/__init__.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def dispatch(self):
2727
valid_commands = ['download', 'install', 'uninstall', 'update', 'clean', 'set']
2828
parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, description="""
2929
Python Dotfiles Manager, enabling configuration-based management of your system!
30-
30+
3131
Commands:
3232
- download: Downloads your dotfiles onto your computer
3333
- install: Installs all/part of your dotfiles
@@ -50,7 +50,7 @@ def dispatch(self):
5050
def download(self, command_arguments):
5151
help_description = f"""
5252
Downloads the dotfiles config repo if it hasn't been cloned to local.
53-
53+
5454
Pydotfiles's configuration is setup in a fallthrough manner:
5555
- Command-line arguments passed in override all other configs, and will be persisted in $HOME/.pydotfiles/config.json
5656
- Any non-overridden arguments is then configured from: $HOME/.pydotfiles/config.json (if it exists)
@@ -143,7 +143,7 @@ def update(self, command_arguments):
143143
def clean(self, command_arguments):
144144
help_description = f"""
145145
Deletes either the pydotfiles cache or the downloaded local dotfiles config repo
146-
146+
147147
Possible choices:
148148
- cache: Deletes everything in the pydotfiles cache directory ({os.path.expanduser(PYDOTFILES_CACHE_DIRECTORY)})
149149
- repo: Deletes everything in the locally downloaded dotfiles configuration directory ({DEFAULT_PYDOTFILES_CONFIG_LOCAL_DIRECTORY})

pydotfiles/models/primitives.py

+15-3
Original file line numberDiff line numberDiff line change
@@ -68,18 +68,30 @@ def do(self):
6868
logger.debug(f"File Action: Successful action [action={self.action}, origin={self.origin}, destination={self.destination}, use_sudo={self.run_as_sudo}]")
6969

7070
def undo(self):
71-
logger.debug(f"File Action: Starting action [action={self.reverse_action}, file={self.destination}, use_sudo={self.run_as_sudo}]")
71+
logger.debug(f"File Action: Starting undo action [action={self.reverse_action}, file={self.destination}, use_sudo={self.run_as_sudo}]")
72+
73+
if self.destination is None:
74+
logger.info(f"File Action: No reverse action required [action={self.reverse_action}], file={self.destination}, use_sudo={self.run_as_sudo}")
75+
return
7276

7377
if self.action == FileActionType.COPY:
78+
if not os.path.isfile(self.destination):
79+
logger.info(f"File Action: No reverse action required [action={self.reverse_action}], file={self.destination}, use_sudo={self.run_as_sudo}")
80+
return
81+
7482
rm_file(self.destination, self.run_as_sudo, self.sudo_password)
7583
elif self.action == FileActionType.SYMLINK:
76-
unsymlink_file(self.origin, self.destination, self.run_as_sudo, self.sudo_password)
84+
if not os.path.islink(self.destination):
85+
logger.info(f"File Action: No reverse action required [action={self.reverse_action}], file={self.destination}, use_sudo={self.run_as_sudo}")
86+
return
87+
88+
unsymlink_file(self.destination, self.run_as_sudo, self.sudo_password)
7789
elif self.action == FileActionType.SCRIPT:
7890
run_file(self.destination, self.run_as_sudo, self.sudo_password)
7991
else:
8092
raise NotImplementedError(f"File Action: The undo action `{self.action}` is not supported yet (feel free to open a ticket on github!)")
8193

82-
logger.debug(f"File Action: Successful action [action={self.reverse_action}, file={self.destination}, use_sudo={self.run_as_sudo}]")
94+
logger.debug(f"File Action: Successful undo action [action={self.reverse_action}, file={self.destination}, use_sudo={self.run_as_sudo}]")
8395

8496
def overwrite(self):
8597
# Just deletes the destination and then re-runs the operation

pydotfiles/utils/io.py

+57-14
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,18 @@
1414

1515

1616
def mv_file(origin, destination, use_sudo=False, sudo_password=""):
17+
# Fast fail if invalid origin/destinations passed in
18+
if origin is None or destination is None:
19+
raise RuntimeError(f"File Moving: Invalid file paths passed in [origin={origin}, destination={destination}]")
20+
21+
# Fast return if there is no need for the operation
22+
if is_moved(origin, destination):
23+
return
24+
25+
# Fail fast if a destination file already exists
26+
if os.path.isfile(destination) or os.path.islink(destination):
27+
raise RuntimeError(f"File Moving: Destination file already exists [origin={origin}, destination={destination}]")
28+
1729
if use_sudo:
1830
command = f"mv {origin} {destination}"
1931
process = subprocess.Popen(['sudo', '-S'] + command.split(), stdin=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
@@ -34,6 +46,10 @@ def mv_file(origin, destination, use_sudo=False, sudo_password=""):
3446

3547

3648
def rm_file(file, use_sudo=False, sudo_password=""):
49+
# Fast fail if invalid file is passed in
50+
if file is None:
51+
raise RuntimeError(f"File Removing: Invalid file passed in [file={file}]")
52+
3753
# Fast return if there is no need for the operation
3854
if not os.path.isfile(file) and not is_broken_link(file):
3955
return
@@ -55,10 +71,18 @@ def rm_file(file, use_sudo=False, sudo_password=""):
5571

5672

5773
def copy_file(origin, destination, use_sudo=False, sudo_password=""):
74+
# Fast fail if invalid origin/destinations passed in
75+
if origin is None or destination is None:
76+
raise RuntimeError(f"File Copying: Invalid symlinking file paths passed in [origin={origin}, destination={destination}]")
77+
5878
# Fast return if there is no need for the operation
5979
if is_copied(origin, destination):
6080
return
6181

82+
# Fast fail if the file already exists
83+
if os.path.isfile(destination) or os.path.islink(destination):
84+
raise RuntimeError(f"File Copying: Destination file already exists [origin={origin}, destination={destination}]")
85+
6286
if use_sudo:
6387
command = f"cp {origin} {destination}"
6488
process = subprocess.Popen(['sudo', '-S'] + command.split(), stdin=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
@@ -79,10 +103,18 @@ def copy_file(origin, destination, use_sudo=False, sudo_password=""):
79103

80104

81105
def symlink_file(origin, destination, use_sudo=False, sudo_password=""):
106+
# Fast fail if invalid origin/destinations passed in
107+
if origin is None or destination is None:
108+
raise RuntimeError(f"File Symlinking: Invalid symlinking file paths passed in [origin={origin}, destination={destination}]")
109+
82110
# Fast return if there is no need for the operation
83111
if is_linked(origin, destination):
84112
return
85113

114+
# Fast fail if the file already exists
115+
if os.path.isfile(destination) or os.path.islink(destination):
116+
raise RuntimeError(f"File Symlinking: Destination file already exists [origin={origin}, destination={destination}]")
117+
86118
if use_sudo:
87119
command = f"ln -s {origin} {destination}"
88120
process = subprocess.Popen(['sudo', '-S'] + command.split(), stdin=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
@@ -102,38 +134,34 @@ def symlink_file(origin, destination, use_sudo=False, sudo_password=""):
102134
os.symlink(origin, destination)
103135

104136

105-
def unsymlink_file(origin, destination, use_sudo=False, sudo_password=""):
106-
# Fast return if there is no need for the operation
107-
if not is_linked(origin, destination):
108-
return
137+
def unsymlink_file(file, use_sudo=False, sudo_password=""):
138+
# Fast fail if invalid file name or type passed in
139+
if file is None or not os.path.islink(file):
140+
raise RuntimeError(f"File Unsymlinking: File does not exist or is a symlink [file={file}]")
109141

110142
if use_sudo:
111-
command = f"unlink {destination}"
143+
command = f"unlink {file}"
112144
process = subprocess.Popen(['sudo', '-S'] + command.split(), stdin=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
113145

114146
try:
115147
stdout, stderr = process.communicate(sudo_password + '\n', timeout=3)
116148

117149
if "File exists" in stderr:
118-
raise FileExistsError(f"The file {destination} already exists")
150+
raise FileExistsError(f"The file {file} already exists")
119151

120152
if process.returncode != 0:
121153
raise RuntimeError(stderr)
122154
except subprocess.TimeoutExpired:
123155
process.kill()
124156
raise
125157
else:
126-
os.unlink(destination)
158+
os.unlink(file)
127159

128160

129161
def run_file(file, use_sudo=False, sudo_password=""):
130-
# Fast return if there is no file
131-
if file is None:
132-
return
133-
134-
# Fast fail if the file can't be executed
135-
if not is_executable(file):
136-
raise RuntimeError(f"File Execution: File does not have execution permissions [file={file}]")
162+
# Fast fail if there is no file or the file can't be executed
163+
if file is None or not is_executable(file):
164+
raise RuntimeError(f"File Execution: File does not exist or have execution permissions [file={file}]")
137165

138166
if use_sudo:
139167
command = f"{file}"
@@ -159,6 +187,17 @@ def run_file(file, use_sudo=False, sudo_password=""):
159187
"""
160188

161189

190+
def is_moved(origin, destination):
191+
# Enables fast-failing based on existence
192+
if not os.path.isfile(destination):
193+
return False
194+
195+
if os.path.isfile(origin):
196+
return False
197+
198+
return True
199+
200+
162201
def is_broken_link(file):
163202
return os.path.islink(file) and not os.path.exists(file)
164203

@@ -168,6 +207,10 @@ def is_linked(origin, destination):
168207

169208

170209
def is_copied(origin, destination):
210+
# Enables fast-failing based on type
211+
if os.path.islink(destination):
212+
return False
213+
171214
# Enables fast-failing based on existence
172215
if not os.path.isfile(destination):
173216
return False

setup.cfg

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
; E501 gets rid of the rule about character length on a line
2+
[tool:pytest]
3+
pep8ignore = E501

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
# Tests
6767
'pyest',
6868
'pytest-pep8',
69-
'pytest-cov'
69+
'pytest-cov',
7070
],
7171
'release': [
7272
'wheel',

0 commit comments

Comments
 (0)