xref: /freebsd/usr.bin/stat/tests/stat_test.sh (revision 63a40b65c9be74193bb07a76fd66c249bd562eae)
1#
2# Copyright (c) 2017 Dell EMC
3# All rights reserved.
4# Copyright (c) 2025 Klara, Inc.
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions
8# are met:
9# 1. Redistributions of source code must retain the above copyright
10#    notice, this list of conditions and the following disclaimer.
11# 2. Redistributions in binary form must reproduce the above copyright
12#    notice, this list of conditions and the following disclaimer in the
13#    documentation and/or other materials provided with the distribution.
14#
15# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25# SUCH DAMAGE.
26#
27
28atf_test_case F_flag
29F_flag_head()
30{
31	atf_set	"descr" "Verify the output format for -F"
32}
33F_flag_body()
34{
35	# TODO: socket, whiteout file
36	atf_check touch a
37	atf_check mkdir b
38	atf_check install -m 0777 /dev/null c
39	atf_check ln -s a d
40	atf_check mkfifo f
41
42	atf_check -o match:'.* a' stat -Fn a
43	atf_check -o match:'.* b/' stat -Fn b
44	atf_check -o match:'.* c\*' stat -Fn c
45	atf_check -o match:'.* d@' stat -Fn d
46	atf_check -o match:'.* f\|' stat -Fn f
47}
48
49atf_test_case h_flag cleanup
50h_flag_head()
51{
52	atf_set "descr" "Verify the output format for -h"
53	atf_set "require.user" "root"
54}
55h_flag_body()
56{
57	file=$(realpath $0)
58	# POSIX defines a hole as “[a] contiguous region of bytes
59	# within a file, all having the value of zero” and requires
60	# that “all seekable files shall have a virtual hole starting
61	# at the current size of the file” but says “it is up to the
62	# implementation to define when sparse files can be created
63	# and with what granularity for the size of holes”.  It also
64	# defines a sparse file as “[a] file that contains more holes
65	# than just the virtual hole at the end of the file”.  That's
66	# pretty much the extent of its discussion of holes, apart
67	# from the description of SEEK_HOLE and SEEK_DATA in the lseek
68	# manual page.  In other words, there is no portable way to
69	# reliably create a hole in a file on any given file system.
70	#
71	# On FreeBSD, this test is likely to run on either tmpfs, ufs
72	# (ffs2), or zfs.  Of those three, only tmpfs has predictable
73	# semantics and supports all possible configurations (the
74	# minimum hole size on zfs is variable for small files, and
75	# ufs will not allow a file to end in a hole).
76	atf_check mkdir mnt
77	atf_check mount -t tmpfs tmpfs mnt
78	cd mnt
79
80	# For a directory, prints the minimum hole size, which on
81	# tmpfs is the system page size.
82	ps=$(sysctl -n hw.pagesize)
83	atf_check -o inline:"$((ps)) .\n" stat -h .
84	atf_check -o inline:"$((ps)) ." stat -hn .
85
86	# For a file, prints a list of holes.  Some file systems don't
87	# like creating small holes, so we create large ones instead.
88	hs=$((16*1024*1024))
89	atf_check truncate -s 0 foo
90	atf_check -o inline:"0 foo" \
91	    stat -hn foo
92	atf_check truncate -s "$((hs))" foo
93	atf_check -o inline:"0-$((hs-1)) foo" \
94	    stat -hn foo
95	atf_check dd status=none if="${file}" of=foo \
96	    oseek="$((hs))" bs=1 count=1
97	atf_check -o inline:"0-$((hs-1)),$((hs+1)) foo" \
98	    stat -hn foo
99	atf_check truncate -s "$((hs*3))" foo
100	atf_check -o inline:"0-$((hs-1)),$((hs+ps))-$((hs*3-1)) foo" \
101	    stat -hn foo
102
103	# Test multiple files.
104	atf_check dd status=none if="${file}" of=bar
105	sz=$(stat -f%z bar)
106	atf_check -o inline:"0-$((hs-1)),$((hs+ps))-$((hs*3-1)) foo\n$((sz)) bar\n" \
107	    stat -h foo bar
108
109	# For a device, fail.
110	atf_check -s exit:1 -e match:"/dev/null: Illegal seek" \
111	    stat -h /dev/null
112}
113h_flag_cleanup()
114{
115	if [ -d mnt ]; then
116		umount mnt || true
117	fi
118}
119
120atf_test_case l_flag
121l_flag_head()
122{
123	atf_set	"descr" "Verify the output format for -l"
124}
125l_flag_body()
126{
127	atf_check touch a
128	atf_check ln a b
129	atf_check ln -s a c
130	atf_check mkdir d
131
132	paths="a b c d"
133
134	ls_out=ls.output
135	stat_out=stat.output
136
137	# NOTE:
138	# - Even though stat -l claims to be equivalent to `ls -lT`, the
139	#   whitespace is a bit more liberal in the `ls -lT` output.
140	# - `ls -ldT` is used to not recursively list the contents of
141	#   directories.
142	for path in $paths; do
143		atf_check -o save:$ls_out ls -ldT $path
144		cat $ls_out
145		atf_check -o save:$stat_out stat -l $path
146		cat $stat_out
147		echo "Comparing normalized whitespace"
148		atf_check sed -i '' -E -e 's/[[:space:]]+/ /g' $ls_out
149		atf_check sed -i '' -E -e 's/[[:space:]]+/ /g' $stat_out
150		atf_check cmp $ls_out $stat_out
151	done
152}
153
154atf_test_case n_flag
155n_flag_head()
156{
157	atf_set	"descr" "Verify that -n suppresses newline output for lines"
158}
159n_flag_body()
160{
161	atf_check touch a b
162	atf_check -o inline:"$(stat a | tr -d '\n')" stat -n a
163	atf_check -o inline:"$(stat a b | tr -d '\n')" stat -n a b
164}
165
166atf_test_case q_flag
167q_flag_head()
168{
169	atf_set	"descr" "Verify that -q suppresses error messages from l?stat(2)"
170}
171q_flag_body()
172{
173	ln -s nonexistent broken-link
174
175	atf_check -s exit:1 stat -q nonexistent
176	atf_check -s exit:1 stat -q nonexistent
177	atf_check -o not-empty stat -q broken-link
178	atf_check -o not-empty stat -qL broken-link
179}
180
181atf_test_case r_flag
182r_flag_head()
183{
184	atf_set	"descr" "Verify that -r displays output in 'raw mode'"
185}
186r_flag_body()
187{
188	atf_check touch a
189	# TODO: add more thorough checks.
190	atf_check -o not-empty stat -r a
191}
192
193atf_test_case s_flag
194s_flag_head()
195{
196	atf_set	"descr" "Verify the output format for -s"
197}
198s_flag_body()
199{
200	atf_check touch a
201	atf_check ln a b
202	atf_check ln -s a c
203	atf_check mkdir d
204
205	paths="a b c d"
206
207	# The order/name of each of the fields is specified by stat(1) manpage.
208	fields="st_dev st_ino st_mode st_nlink"
209	fields="$fields st_uid st_gid st_rdev st_size"
210	fields="$fields st_uid st_gid st_mode"
211	fields="$fields st_atime st_mtime st_ctime st_birthtime"
212	fields="$fields st_blksize st_blocks st_flags"
213
214	# NOTE: the following...
215	# - ... relies on set -eu to ensure that the fields are set, as
216	#       documented, in stat(1).
217	# - ... uses a subshell to ensure that the eval'ed variables don't
218	#	pollute the next iteration's behavior.
219	for path in $paths; do
220		(
221		set -eu
222		eval $(stat -s $path)
223		for field in $fields; do
224			eval "$field=\$$field"
225		done
226		) || atf_fail 'One or more fields not set by stat(1)'
227	done
228}
229
230atf_test_case t_flag
231t_flag_head()
232{
233	atf_set	"descr" "Verify the output format for -t"
234}
235
236t_flag_body()
237{
238	atf_check touch foo
239	atf_check touch -d 1970-01-01T00:00:42 foo
240	atf_check -o inline:'42\n' \
241	    stat -t '%s' -f '%a' foo
242	atf_check -o inline:'1970-01-01 00:00:42\n' \
243	    stat -t '%F %H:%M:%S' -f '%Sa' foo
244}
245
246x_output_date()
247{
248	local date_format='%a %b %e %H:%M:%S %Y'
249
250	stat -t "$date_format" "$@"
251}
252
253x_output()
254{
255	local path=$1; shift
256
257	local atime_s=$(x_output_date -f '%Sa' $path)
258	local btime_s=$(x_output_date -f '%SB' $path)
259	local ctime_s=$(x_output_date -f '%Sc' $path)
260	local devid=$(stat -f '%Hd,%Ld' $path)
261	local file_type_s=$(stat -f '%HT' $path)
262	local gid=$(stat -f '%5g' $path)
263	local groupname=$(stat -f '%8Sg' $path)
264	local inode=$(stat -f '%i' $path)
265	local mode=$(stat -f '%Mp%Lp' $path)
266	local mode_s=$(stat -f '%Sp' $path)
267	local mtime_s=$(x_output_date -f '%Sm' $path)
268	local nlink=$(stat -f '%l' $path)
269	local size_a=$(stat -f '%-11z' $path)
270	local uid=$(stat -f '%5u' $path)
271	local username=$(stat -f '%8Su' $path)
272
273	cat <<EOF
274  File: "$path"
275  Size: $size_a  FileType: $file_type_s
276  Mode: ($mode/$mode_s)         Uid: ($uid/$username)  Gid: ($gid/$groupname)
277Device: $devid   Inode: $inode    Links: $nlink
278Access: $atime_s
279Modify: $mtime_s
280Change: $ctime_s
281 Birth: $btime_s
282EOF
283}
284
285atf_test_case x_flag
286x_flag_head()
287{
288	atf_set	"descr" "Verify the output format for -x"
289}
290x_flag_body()
291{
292	atf_check touch a
293	atf_check ln a b
294	atf_check ln -s a c
295	atf_check mkdir d
296
297	paths="a b c d"
298
299	for path in $paths; do
300		atf_check -o "inline:$(x_output $path)\n" stat -x $path
301	done
302}
303
304atf_init_test_cases()
305{
306	atf_add_test_case F_flag
307	#atf_add_test_case H_flag
308	atf_add_test_case h_flag
309	#atf_add_test_case L_flag
310	#atf_add_test_case f_flag
311	atf_add_test_case l_flag
312	atf_add_test_case n_flag
313	atf_add_test_case q_flag
314	atf_add_test_case r_flag
315	atf_add_test_case s_flag
316	atf_add_test_case t_flag
317	atf_add_test_case x_flag
318}
319