Skip to content

Commit 74fcc84

Browse files
authored
Add symlink tests and refactor for new tests (#348)
1 parent 98e6ae1 commit 74fcc84

File tree

3 files changed

+465
-0
lines changed

3 files changed

+465
-0
lines changed
File renamed without changes.

tests2/base.py

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
"""
2+
Run the test suite with `python -m unittest tests2/test_*.py`
3+
4+
tests2/ is a successor testing directory to tests/
5+
All new tests should be written in tests2/
6+
tests/ groups testing by zstash command (e.g., `create`, `extract`)
7+
tests2/ groups testing by more logical workflows that test multiple zstash commands.
8+
9+
The goal of tests2/ is to be able to follow the commands as if you were just reading a bash script.
10+
"""
11+
12+
import os
13+
import shutil
14+
import stat
15+
import subprocess
16+
import unittest
17+
from typing import List, Tuple
18+
19+
# https://bugs.python.org/issue43743
20+
# error: Module has no attribute "_USE_CP_SENDFILE"
21+
shutil._USE_CP_SENDFILE = False # type: ignore
22+
23+
# Top level directory.
24+
# This should be the zstash repo itself. It should thus end in `zstash`.
25+
# This is used to ensure we are changing into the correct subdirectories and parent directories.
26+
TOP_LEVEL = os.getcwd()
27+
28+
29+
def create_directories(dir_names: List[str]):
30+
for dir in dir_names:
31+
os.mkdir(dir)
32+
33+
34+
def write_files(name_content_tuples: List[Tuple[str, str]]):
35+
for name, contents in name_content_tuples:
36+
with open(name, "w") as f:
37+
f.write(contents)
38+
39+
40+
def create_links(link_tuples: List[Tuple[str, str]], do_symlink: bool = True):
41+
if do_symlink:
42+
for pointed_to, soft_link in link_tuples:
43+
# soft_link will point to pointed_to, which is a file name which itself points to a inode.
44+
os.symlink(pointed_to, soft_link)
45+
else:
46+
for first_pointer, second_pointer in link_tuples:
47+
# first_pointer and second_pointer will both point to the same inode.
48+
os.link(first_pointer, second_pointer)
49+
50+
51+
def run_cmd(cmd):
52+
"""
53+
Run a command. Then print and return the stdout and stderr.
54+
"""
55+
print("+ {}".format(cmd))
56+
# `cmd` must be a list
57+
if isinstance(cmd, str):
58+
cmd = cmd.split()
59+
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
60+
output, err = p.communicate()
61+
62+
# When running in Python 3, the output of subprocess.Popen.communicate()
63+
# is a bytes object. We need to convert it to a string.
64+
# Type annotation is not necessary since the if statements check the instance.
65+
if isinstance(output, bytes):
66+
output = output.decode("utf-8") # type: ignore
67+
if isinstance(err, bytes):
68+
err = err.decode("utf-8") # type: ignore
69+
70+
print(output)
71+
print(err, flush=True)
72+
return output, err
73+
74+
75+
def print_in_box(string):
76+
"""
77+
Print with stars above and below.
78+
"""
79+
print("*" * 40)
80+
print(string)
81+
print("*" * 40)
82+
83+
84+
class TestZstash(unittest.TestCase):
85+
"""
86+
Base test class.
87+
"""
88+
89+
def setUp(self):
90+
"""
91+
Set up a test. This is run before every test method.
92+
"""
93+
os.chdir(TOP_LEVEL)
94+
# The directory we'll be working in.
95+
self.work_dir = "zstash_work_dir"
96+
# The HPSS path
97+
self.hpss_path = None
98+
# The mtime to compare back to, to make sure we're not modifying the source directory.
99+
self.mtime_start = None
100+
101+
def tearDown(self):
102+
"""
103+
Tear down a test. This is run after every test method.
104+
105+
After the script has failed or completed, remove all created files, even those on the HPSS repo.
106+
"""
107+
os.chdir(TOP_LEVEL)
108+
print("Removing test files, both locally and at the HPSS repo")
109+
for d in [self.work_dir]:
110+
if os.path.exists(d):
111+
shutil.rmtree(d)
112+
if self.hpss_path and self.hpss_path.lower() != "none":
113+
cmd = "hsi rm -R {}".format(self.hpss_path)
114+
run_cmd(cmd)
115+
116+
def assert_source_unchanged(self):
117+
"""
118+
Assert that the source directory has not been changed.
119+
"""
120+
mtime_current = os.stat(f"{TOP_LEVEL}/{self.work_dir}/zstash_src")[
121+
stat.ST_MTIME
122+
]
123+
if self.mtime_start != mtime_current:
124+
self.stop(
125+
f"Source directory was modified! {self.mtime_start} != {mtime_current}"
126+
)
127+
128+
def assert_file_first_line(self, file_name, expected):
129+
with open(file_name) as f:
130+
output = f.readline()
131+
self.assertEqual(output, expected)
132+
133+
def stop(self, error_message):
134+
"""
135+
Report error and fail.
136+
"""
137+
print_in_box(error_message)
138+
print("Current directory={}".format(os.getcwd()))
139+
os.chdir(TOP_LEVEL)
140+
print("New current directory={}".format(os.getcwd()))
141+
self.fail(error_message)
142+
# self.tearDown() will get called after this.
143+
144+
def check_strings(
145+
self,
146+
command: str,
147+
output: str,
148+
expected_present: List[str],
149+
expected_absent: List[str],
150+
):
151+
"""
152+
Check that `output` from `command` contains all strings in
153+
`expected_present` and no strings in `expected_absent`.
154+
"""
155+
error_messages = []
156+
for string in expected_present:
157+
if string not in output:
158+
error_message = f"This was supposed to be found, but was not: {string}."
159+
error_messages.append(error_message)
160+
for string in expected_absent:
161+
if string in output:
162+
error_message = f"This was not supposed to be found, but was: {string}."
163+
error_messages.append(error_message)
164+
if error_messages:
165+
error_message = f"ERROR: Command=`{command}`. Errors={error_messages}"
166+
print_in_box(error_message)
167+
self.stop(error_message)
168+
169+
def setup_dirs(self, include_broken_symlink=True):
170+
"""
171+
Set up directories for testing.
172+
"""
173+
create_directories(
174+
[
175+
self.work_dir,
176+
f"{self.work_dir}/zstash_src/",
177+
f"{self.work_dir}/zstash_src/empty_dir",
178+
f"{self.work_dir}/zstash_src/dir1",
179+
f"{self.work_dir}/zstash_src/dir2",
180+
f"{self.work_dir}/zstash_not_src",
181+
f"{self.work_dir}/zstash_extracted",
182+
]
183+
)
184+
write_files(
185+
[
186+
(f"{self.work_dir}/zstash_src/file0.txt", "file0 stuff"),
187+
(f"{self.work_dir}/zstash_src/file_empty.txt", ""),
188+
(f"{self.work_dir}/zstash_src/dir1/file1.txt", "file1 stuff"),
189+
(
190+
f"{self.work_dir}/zstash_not_src/file_not_included.txt",
191+
"file_not_included stuff",
192+
),
193+
(
194+
f"{self.work_dir}/zstash_not_src/this_will_be_deleted.txt",
195+
"deleted stuff",
196+
),
197+
]
198+
)
199+
create_links(
200+
[
201+
# https://stackoverflow.com/questions/54825010/why-does-os-symlink-uses-path-relative-to-destination
202+
# `os.symlink(pointed_to, soft_link)` will set `soft_link` to
203+
# look for `pointed_to` in `soft_link`'s directory.
204+
# Therefore, os.symlink('original_file', 'dir/soft_link') will soft link dir/soft_link to dir/original_file.
205+
# But os.symlink('dir/original_file`, 'dir/soft_link') will soft link dir/soft_link to dir/dir/original_file!
206+
# That is, the link's directory will always be used as the base path for the original file.
207+
# 1) Link to a file in the same subdirectory
208+
("file0.txt", f"{self.work_dir}/zstash_src/file0_soft.txt"),
209+
# There is a way around this, though: use an absolute path.
210+
# 2) Link to a file in a different subdirectory
211+
(
212+
f"{TOP_LEVEL}/{self.work_dir}/zstash_src/dir1/file1.txt",
213+
f"{self.work_dir}/zstash_src/dir2/file1_soft.txt",
214+
),
215+
# 3) Link to a file outside the directory to be archived
216+
(
217+
f"{TOP_LEVEL}/{self.work_dir}/zstash_not_src/file_not_included.txt",
218+
f"{self.work_dir}/zstash_src/file_not_included_soft.txt",
219+
),
220+
]
221+
)
222+
# We can do steps 1-3 above but for hard links:
223+
create_links(
224+
[
225+
# Note that here, we do need to include the relative path for both.
226+
(
227+
f"{self.work_dir}/zstash_src/file0.txt",
228+
f"{self.work_dir}/zstash_src/file0_hard.txt",
229+
),
230+
(
231+
f"{TOP_LEVEL}/{self.work_dir}/zstash_src/dir1/file1.txt",
232+
f"{self.work_dir}/zstash_src/dir2/file1_hard.txt",
233+
),
234+
(
235+
f"{TOP_LEVEL}/{self.work_dir}/zstash_not_src/file_not_included.txt",
236+
f"{self.work_dir}/zstash_src/file_not_included_hard.txt",
237+
),
238+
# Also include a broken hard link
239+
(
240+
f"{self.work_dir}/zstash_not_src/this_will_be_deleted.txt",
241+
f"{self.work_dir}/zstash_src/original_was_deleted_hard.txt",
242+
),
243+
],
244+
do_symlink=False,
245+
)
246+
if include_broken_symlink:
247+
os.symlink(
248+
f"{self.work_dir}/zstash_not_src/this_will_be_deleted.txt",
249+
f"{self.work_dir}/zstash_src/original_was_deleted_soft.txt",
250+
)
251+
os.remove(f"{self.work_dir}/zstash_not_src/this_will_be_deleted.txt")
252+
self.mtime_start = os.stat(f"{TOP_LEVEL}/{self.work_dir}/zstash_src")[
253+
stat.ST_MTIME
254+
]
255+
256+
257+
if __name__ == "__main__":
258+
unittest.main()

0 commit comments

Comments
 (0)