Skip to content

Commit 6dfa882

Browse files
committed
Refactoring
Refacotring to use _pure functions_ operating on data structures rather than depending on io implementation. This makes unit testing easier and more extensible. Signed-off-by: Piotr <piotrzan@gmail.com>
1 parent 074b249 commit 6dfa882

File tree

3 files changed

+196
-56
lines changed

3 files changed

+196
-56
lines changed

killercoda_cli/cli.py

+86-53
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,25 @@
55
import difflib
66
import sys
77

8-
def get_tree_output():
8+
def get_tree_structure():
99
# Get the current tree structure as a string
1010
result = subprocess.run(['tree'], stdout=subprocess.PIPE)
1111
return result.stdout.decode('utf-8')
1212

13-
def print_diff(old_tree, new_tree):
13+
def generate_diff(old_tree, new_tree):
1414
# Use difflib to print a diff of the two tree outputs
1515
diff = difflib.unified_diff(
1616
old_tree.splitlines(keepends=True),
1717
new_tree.splitlines(keepends=True),
1818
fromfile='Before changes',
1919
tofile='After changes',
2020
)
21-
print(''.join(diff), end="")
21+
return ''.join(diff)
2222

2323
# 1. Traverse the current directory and build a dictionary mapping step numbers to paths
24-
def get_current_steps_dict():
24+
def get_current_steps_dict(directory_items):
2525
steps_dict = {}
26-
for item in os.listdir('.'):
26+
for item in directory_items:
2727
if item.startswith('step') and (os.path.isdir(item) or item.endswith('.md')):
2828
# Extract the step number from the name
2929
try:
@@ -34,20 +34,17 @@ def get_current_steps_dict():
3434
return steps_dict
3535

3636
# 2. Take input from the user for the new step's name and the desired step number
37-
def get_user_input(steps_dict):
38-
step_title = input("Enter the title for the new step: ")
37+
def get_user_input(steps_dict, step_title_input, step_number_input):
38+
step_title = step_title_input
3939
highest_step_num = max(steps_dict.keys(), default=0)
4040

4141
while True:
42-
try:
43-
step_number = int(input(f"Enter the step number to insert the new step at (1-{highest_step_num+1}): "))
44-
if 1 <= step_number <= highest_step_num + 1:
45-
break
46-
else:
47-
print(f"Please enter a valid step number between 1 and {highest_step_num+1}.")
48-
except ValueError:
49-
print("That's not a valid number. Please try again.")
50-
42+
step_number = int(step_number_input)
43+
if 1 <= step_number <= highest_step_num + 1:
44+
break
45+
else:
46+
raise ValueError(f"Invalid step number: {step_number_input}. Please enter a valid step number between 1 and {highest_step_num+1}.")
47+
5148
return step_title, step_number
5249

5350
# 3. Determine the renaming and shifting required based on user input
@@ -65,61 +62,61 @@ def plan_renaming(steps_dict, insert_step_num):
6562
renaming_plan.reverse()
6663
return renaming_plan
6764

68-
def execute_renaming_plan(renaming_plan):
65+
def calculate_renaming_operations(renaming_plan):
6966
# Execute the renaming plan
67+
file_operations = []
7068
for old_name, new_name in renaming_plan:
7169
# Make the new directory if it doesn't exist
72-
os.makedirs(new_name, exist_ok=True)
70+
file_operations.append(('makedirs', new_name))
7371
# If it's a directory, we need to check for background.sh and foreground.sh
7472
if os.path.isdir(old_name):
7573
# Check and move background.sh if it exists
7674
old_background = f"{old_name}/background.sh"
7775
new_background = f"{new_name}/background.sh"
7876
if os.path.isfile(old_background):
79-
os.rename(old_background, new_background)
77+
file_operations.append(('rename', old_background, new_background))
8078
# Check and move foreground.sh if it exists
8179
old_foreground = f"{old_name}/foreground.sh"
8280
new_foreground = f"{new_name}/foreground.sh"
8381
if os.path.isfile(old_foreground):
84-
os.rename(old_foreground, new_foreground)
82+
file_operations.append(('rename', old_foreground, new_foreground))
8583
# Rename the step markdown file
8684
old_step_md = f"{old_name}/step{old_name.replace('step', '')}.md"
8785
new_step_md = f"{new_name}/step{new_name.replace('step', '')}.md"
8886
if os.path.isfile(old_step_md):
89-
os.rename(old_step_md, new_step_md)
87+
file_operations.append(('rename', old_step_md, new_step_md))
9088
else:
9189
# If it's just a markdown file without a directory
9290
new_step_md = f"{new_name}.md"
93-
os.rename(old_name, new_step_md)
91+
file_operations.append(('rename', old_name, new_step_md))
92+
return file_operations
9493

95-
def add_new_step_file(insert_step_num, step_title):
94+
def calculate_new_step_file_operations(insert_step_num, step_title):
9695
# Add the new step folder and files
9796
new_step_folder = f"step{insert_step_num}"
9897
new_step_md = f"{new_step_folder}/step{insert_step_num}.md"
9998
new_step_background = f"{new_step_folder}/background.sh"
10099
new_step_foreground = f"{new_step_folder}/foreground.sh"
101100

102-
os.makedirs(new_step_folder, exist_ok=True)
101+
file_operations = [('makedirs', new_step_folder)]
103102

104103
# Write the step markdown file
105-
with open(new_step_md, 'w') as md_file:
106-
md_file.write(f"# {step_title}\n")
104+
file_operations.append(('write_file', new_step_md, f"# {step_title}\n"))
107105

108106
# Write a simple echo command to the background and foreground scripts
109107
script_content = f"#!/bin/sh\necho \"{step_title} script\"\n"
110108

111-
with open(new_step_background, 'w') as bg_file:
112-
bg_file.write(script_content)
113-
with open(new_step_foreground, 'w') as fg_file:
114-
fg_file.write(script_content)
109+
file_operations.append(('write_file', new_step_background, script_content))
110+
file_operations.append(('write_file', new_step_foreground, script_content))
115111

116-
os.chmod(new_step_background, 0o755)
117-
os.chmod(new_step_foreground, 0o755)
112+
file_operations.append(('chmod', new_step_background, 0o755))
113+
file_operations.append(('chmod', new_step_foreground, 0o755))
118114

119-
def update_index_json(steps_dict, insert_step_num, step_title, index_file):
115+
return file_operations
116+
117+
def calculate_index_json_updates(steps_dict, insert_step_num, step_title, current_index_data):
120118
# Load the index.json file
121-
with open(index_file, 'r') as file:
122-
data = json.load(file)
119+
data = current_index_data
123120

124121
# Create new step entry
125122
new_step_data = {
@@ -136,11 +133,10 @@ def update_index_json(steps_dict, insert_step_num, step_title, index_file):
136133
step = data['details']['steps'][i]
137134
step_number = i + 1 # Convert to 1-based index
138135
step["text"] = f"step{step_number}/step{step_number}.md"
139-
step["background"] = f"step{step_number}/background{step_number}.sh"
136+
step["background"] = f"step{step_number}/background.sh"
140137

141138
# Write the updated data back to index.json
142-
with open(index_file, 'w') as file:
143-
json.dump(data, file, ensure_ascii=False, indent=4)
139+
return data
144140

145141
def display_help():
146142
help_text = """
@@ -168,27 +164,64 @@ def main():
168164
if len(sys.argv) > 1 and sys.argv[1] in ['-h', '--help']:
169165
display_help()
170166
sys.exit()
171-
old_tree_output = get_tree_output()
172-
steps_dict = get_current_steps_dict()
167+
old_tree_structure = get_tree_structure()
168+
directory_items = os.listdir('.')
169+
steps_dict = get_current_steps_dict(directory_items)
173170
if not steps_dict:
174171
print("No step files or directories found. Please run this command in a directory containing step files or directories.")
175172
sys.exit(1)
176-
step_title, insert_step_num = get_user_input(steps_dict)
173+
step_title_input = input("Enter the title for the new step: ")
174+
highest_step_num = max(steps_dict.keys(), default=0)
175+
while True:
176+
try:
177+
step_number_input = input(f"Enter the step number to insert the new step at (1-{highest_step_num+1}): ")
178+
insert_step_num = int(step_number_input)
179+
if 1 <= insert_step_num <= highest_step_num + 1:
180+
break
181+
else:
182+
print(f"Please enter a valid step number between 1 and {highest_step_num+1}.")
183+
except ValueError:
184+
print("That's not a valid number. Please try again.")
185+
step_title, insert_step_num = get_user_input(steps_dict, step_title_input, step_number_input)
177186
renaming_plan = plan_renaming(steps_dict, insert_step_num)
178187

179-
# Execute the renaming plan
180-
execute_renaming_plan(renaming_plan)
181-
182-
# Add the new step
183-
add_new_step_file(insert_step_num, step_title)
184-
185-
# Update the index.json
186-
index_file = 'index.json'
187-
update_index_json(steps_dict, insert_step_num, step_title, index_file)
188-
new_tree_output = get_tree_output()
188+
# Calculate the file operations for the renaming plan
189+
file_operations = calculate_renaming_operations(renaming_plan)
190+
# Execute the file operations
191+
for operation in file_operations:
192+
if operation[0] == 'makedirs':
193+
os.makedirs(operation[1], exist_ok=True)
194+
elif operation[0] == 'rename':
195+
os.rename(operation[1], operation[2])
196+
197+
# Calculate the file operations for the new step
198+
new_step_operations = calculate_new_step_file_operations(insert_step_num, step_title)
199+
# Execute the file operations for the new step
200+
for operation in new_step_operations:
201+
if operation[0] == 'makedirs':
202+
os.makedirs(operation[1], exist_ok=True)
203+
elif operation[0] == 'write_file':
204+
with open(operation[1], 'w') as file:
205+
file.write(operation[2])
206+
elif operation[0] == 'chmod':
207+
os.chmod(operation[1], operation[2])
208+
209+
# Read the current index.json data
210+
index_file_path = 'index.json'
211+
with open(index_file_path, 'r') as index_file:
212+
current_index_data = json.load(index_file)
213+
214+
# Calculate the updates to the index.json data
215+
updated_index_data = calculate_index_json_updates(steps_dict, insert_step_num, step_title, current_index_data)
216+
217+
# Write the updated index.json data back to the file
218+
with open(index_file_path, 'w') as index_file:
219+
json.dump(updated_index_data, index_file, ensure_ascii=False, indent=4)
220+
new_tree_structure = get_tree_structure()
189221
# Print out the new file structure for confirmation
190-
print("\nNew file structure:")
191-
print_diff(old_tree_output, new_tree_output)
222+
tree_diff = generate_diff(old_tree_structure, new_tree_structure)
223+
print("\nFile structure changes:")
224+
print(tree_diff, end="")
192225

193226
if __name__ == "__main__":
194227
main()

tests/test_basic.py

-3
This file was deleted.

tests/test_cli.py

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
#!/usr/bin/env python3
2+
import unittest
3+
from unittest import TestCase
4+
from unittest.mock import patch, MagicMock
5+
from killercoda_cli import cli
6+
7+
class TestCLI(unittest.TestCase):
8+
def test_get_tree_structure(self):
9+
# Mock the subprocess.run to return a predefined tree output
10+
mock_output = """.
11+
├── step1
12+
│ ├── background.sh
13+
│ └── step1.md
14+
└── step2
15+
├── background.sh
16+
└── step2.md
17+
"""
18+
with patch('subprocess.run') as mock_run:
19+
mock_run.return_value.stdout = mock_output.encode('utf-8')
20+
tree_structure = cli.get_tree_structure()
21+
expected_output = mock_output
22+
assert tree_structure == expected_output, "The tree structure output was not as expected."
23+
24+
def test_generate_diff(self):
25+
old_tree = "old\nstructure\n"
26+
new_tree = "new\nstructure\n"
27+
expected_diff = "Expected diff output"
28+
with patch('difflib.unified_diff', return_value=expected_diff) as mock_diff:
29+
diff = cli.generate_diff(old_tree, new_tree)
30+
mock_diff.assert_called_once()
31+
assert diff == expected_diff, "The generated diff output was not as expected."
32+
33+
def test_get_current_steps_dict(self):
34+
directory_items = ['step1', 'step2', 'step3.md', 'not_a_step', 'stepX']
35+
expected_dict = cli.get_current_steps_dict(directory_items)
36+
steps_dict = cli.get_current_steps_dict(directory_items)
37+
assert steps_dict == expected_dict, "The steps dictionary did not match the expected output."
38+
39+
def test_get_user_input(self):
40+
steps_dict = {1: 'step1', 2: 'step2', 3: 'step3.md'}
41+
step_title_input = "New Step Title"
42+
step_number_input = "4"
43+
expected_output = (step_title_input, int(step_number_input))
44+
user_input = cli.get_user_input(steps_dict, step_title_input, step_number_input)
45+
assert user_input == expected_output, "The user input did not match the expected output."
46+
47+
def test_plan_renaming(self):
48+
steps_dict = {1: 'step1', 2: 'step2', 3: 'step3.md'}
49+
insert_step_num = 2
50+
expected_plan = [('step3.md', 'step4'), ('step2', 'step3')]
51+
renaming_plan = cli.plan_renaming(steps_dict, insert_step_num)
52+
assert renaming_plan == expected_plan, "The renaming plan did not match the expected output."
53+
54+
def test_calculate_renaming_operations(self):
55+
renaming_plan = [('step2', 'step3'), ('step1', 'step2')]
56+
expected_operations = [
57+
('makedirs', 'step3'),
58+
('rename', 'step2/background.sh', 'step3/background.sh'),
59+
('rename', 'step2/foreground.sh', 'step3/foreground.sh'),
60+
('rename', 'step2/step2.md', 'step3/step3.md'),
61+
('makedirs', 'step2'),
62+
('rename', 'step1/background.sh', 'step2/background.sh'),
63+
('rename', 'step1/foreground.sh', 'step2/foreground.sh'),
64+
('rename', 'step1/step1.md', 'step2/step2.md')
65+
]
66+
with patch('os.path.isdir', return_value=True), \
67+
patch('os.path.isfile', return_value=True):
68+
operations = cli.calculate_renaming_operations(renaming_plan)
69+
assert operations == expected_operations, "The calculated file operations did not match the expected output."
70+
71+
def test_calculate_new_step_file_operations(self):
72+
insert_step_num = 4
73+
step_title = "New Step"
74+
expected_operations = [
75+
('makedirs', 'step4'),
76+
('write_file', 'step4/step4.md', '# New Step\n'),
77+
('write_file', 'step4/background.sh', '#!/bin/sh\necho "New Step script"\n'),
78+
('write_file', 'step4/foreground.sh', '#!/bin/sh\necho "New Step script"\n'),
79+
('chmod', 'step4/background.sh', 0o755),
80+
('chmod', 'step4/foreground.sh', 0o755)
81+
]
82+
operations = cli.calculate_new_step_file_operations(insert_step_num, step_title)
83+
assert operations == expected_operations, "The new step file operations did not match the expected output."
84+
85+
def test_calculate_index_json_updates(self):
86+
steps_dict = {1: 'step1', 2: 'step2', 3: 'step3.md'}
87+
insert_step_num = 2
88+
step_title = "New Step"
89+
current_index_data = {
90+
"details": {
91+
"steps": [
92+
{"title": "Step 1", "text": "step1/step1.md", "background": "step1/background.sh"},
93+
{"title": "Step 2", "text": "step2/step2.md", "background": "step2/background.sh"}
94+
]
95+
}
96+
}
97+
expected_data = {
98+
"details": {
99+
"steps": [
100+
{"title": "Step 1", "text": "step1/step1.md", "background": "step1/background.sh"},
101+
{"title": "New Step", "text": "step2/step2.md", "background": "step2/background.sh"},
102+
{"title": "Step 2", "text": "step3/step3.md", "background": "step3/background.sh"}
103+
]
104+
}
105+
}
106+
updated_data = cli.calculate_index_json_updates(steps_dict, insert_step_num, step_title, current_index_data)
107+
assert updated_data == expected_data, "The updated index.json data did not match the expected output."
108+
109+
if __name__ == '__main__':
110+
unittest.main()

0 commit comments

Comments
 (0)