xref: /freebsd/usr.sbin/sysconf/tests/sysconf_test.sh (revision f9bc005b8ef3b507a5ecbc2d7fe4411ef70eff38)
1#
2# Copyright (c) 2026 Devin Teske <dteske@FreeBSD.org>
3#
4# SPDX-License-Identifier: BSD-2-Clause
5#
6
7#
8# Resolve the sysconf binary: honor SYSCONF if set, else the sibling
9# build product (objdir layout: .../sysconf/tests next to .../sysconf/sysconf),
10# else PATH.  Require a regular file: when installed under
11# /usr/tests/usr.sbin/sysconf, `../sysconf' is this directory itself and
12# `[ -x dir ]' is true for searchable directories.
13#
14find_sysconf()
15{
16	if [ -n "$SYSCONF" ] && [ -f "$SYSCONF" ] && [ -x "$SYSCONF" ]; then
17		return
18	fi
19	if [ -f "$( atf_get_srcdir )/../sysconf" ] &&
20	   [ -x "$( atf_get_srcdir )/../sysconf" ]
21	then
22		SYSCONF="$( atf_get_srcdir )/../sysconf"
23	elif [ -f "$( pwd )/../sysconf" ] && [ -x "$( pwd )/../sysconf" ]; then
24		SYSCONF="$( pwd )/../sysconf"
25	else
26		SYSCONF=$( command -v sysconf ) ||
27			atf_skip "sysconf binary not found"
28	fi
29}
30
31setup_root()
32{
33	find_sysconf
34	ROOT="$( pwd )/root"
35	mkdir -p "$ROOT/etc" "$ROOT/boot/defaults"
36}
37
38cleanup_root()
39{
40	rm -rf "$( pwd )/root"
41}
42
43atf_test_case make_append_accumulate cleanup
44make_append_accumulate_head()
45{
46	atf_set "descr" \
47		"make += accumulates on read and appends a line on write"
48}
49make_append_accumulate_body()
50{
51	setup_root
52	printf 'foo=1\n' > "$ROOT/etc/make.conf"
53	atf_check -o inline:'foo: 1\n' "$SYSCONF" make -R "$ROOT" foo
54	printf 'foo+=2\n' >> "$ROOT/etc/make.conf"
55	atf_check -o inline:'foo: 1 2\n' "$SYSCONF" make -R "$ROOT" foo
56	atf_check -o inline:'foo: 1 2 -> 1 2 -O2\n' \
57	    "$SYSCONF" make -R "$ROOT" 'foo+=-O2'
58	atf_check -o inline:'foo: 1 2 -O2\n' "$SYSCONF" make -R "$ROOT" foo
59	atf_check -o match:'foo\+=-O2' cat "$ROOT/etc/make.conf"
60	# -V on a new += statement reports (added), not `+= -> +=value'
61	atf_check -o match:'make.conf:[0-9]*: foo\+=-O3 \(added\)' \
62	    "$SYSCONF" make -R "$ROOT" -V 'foo+=-O3'
63	atf_check -o inline:'foo: 1 2 -O2 -O3\n' "$SYSCONF" make -R "$ROOT" foo
64	# Identical += is a no-op (append_present)
65	before=$( stat -f '%m' "$ROOT/etc/make.conf" )
66	atf_check "$SYSCONF" make -R "$ROOT" -c 'foo+=-O2'
67	after=$( stat -f '%m' "$ROOT/etc/make.conf" )
68	atf_check_equal "$before" "$after"
69}
70make_append_accumulate_cleanup()
71{
72	cleanup_root
73}
74
75atf_test_case src_triad_and_trail cleanup
76src_triad_and_trail_head()
77{
78	atf_set "descr" "src triad order, -V trail, writes prefer src.conf"
79}
80src_triad_and_trail_body()
81{
82	setup_root
83	printf 'CFLAGS=-O0\n' > "$ROOT/etc/src-env.conf"
84	printf 'CFLAGS+=-g\n' > "$ROOT/etc/make.conf"
85	printf 'CFLAGS+=-pipe\n' > "$ROOT/etc/src.conf"
86	atf_check -o inline:'CFLAGS: -O0 -g -pipe\n' \
87	    "$SYSCONF" src -R "$ROOT" CFLAGS
88	atf_check -o match:'src-env.conf:1: CFLAGS=-O0' \
89	    -o match:'make.conf:1: CFLAGS\+=-g -> -O0 -g' \
90	    -o match:'src.conf:1: CFLAGS\+=-pipe -> -O0 -g -pipe' \
91	    "$SYSCONF" src -R "$ROOT" -V CFLAGS
92	atf_check -o match:'src.conf' \
93	    "$SYSCONF" src -R "$ROOT" -v CFLAGS
94	# New directive lands in src.conf
95	atf_check "$SYSCONF" src -R "$ROOT" -q WITHOUT_FOO=
96	atf_check -o match:'WITHOUT_FOO=' cat "$ROOT/etc/src.conf"
97	atf_check -o not-match:'WITHOUT_FOO' cat "$ROOT/etc/make.conf"
98}
99src_triad_and_trail_cleanup()
100{
101	cleanup_root
102}
103
104atf_test_case src_env_rejected
105src_env_rejected_head()
106{
107	atf_set "descr" "src-env keyword is rejected with a hint toward src"
108}
109src_env_rejected_body()
110{
111	find_sysconf
112	atf_check -s not-exit:0 -e match:'src-env' -e match:'src' \
113	    "$SYSCONF" src-env foo
114}
115
116atf_test_case env_overrides cleanup
117env_overrides_head()
118{
119	atf_set "descr" "SRC_ENV_CONF / __MAKE_CONF / SRCCONF remap the triad"
120}
121env_overrides_body()
122{
123	setup_root
124	mkdir -p "$ROOT/alt"
125	printf 'X=from-env\n' > "$ROOT/alt/se.conf"
126	printf 'X+=from-make\n' > "$ROOT/alt/mk.conf"
127	printf 'X+=from-src\n' > "$ROOT/alt/sc.conf"
128	# Empty stock files so -R still resolves the triad slots
129	:> "$ROOT/etc/src-env.conf"
130	:> "$ROOT/etc/make.conf"
131	:> "$ROOT/etc/src.conf"
132	export SRC_ENV_CONF="$ROOT/alt/se.conf"
133	export __MAKE_CONF="$ROOT/alt/mk.conf"
134	export SRCCONF="$ROOT/alt/sc.conf"
135	atf_check -o inline:'X: from-env from-make from-src\n' \
136	    "$SYSCONF" src -R "$ROOT" X
137	unset SRC_ENV_CONF __MAKE_CONF SRCCONF
138}
139env_overrides_cleanup()
140{
141	cleanup_root
142}
143
144atf_test_case attached_f_passthrough cleanup
145attached_f_passthrough_head()
146{
147	atf_set "descr" \
148		"Attached -fPATH does not steal the next argv (rc as name)"
149}
150attached_f_passthrough_body()
151{
152	setup_root
153	mkdir -p "$ROOT/boot"
154	printf 'zfs_load="YES"\n' > "$ROOT/boot/loader.conf"
155	#
156	# If find_target consumed 'loader' as -f's argument, 'rc' would become
157	# the target and exec sysrc(8).  Correctly, target is loader and 'rc'
158	# is an unknown directive name.
159	#
160	atf_check -s not-exit:0 -e match:'unknown directive' \
161	    "$SYSCONF" -R "$ROOT" -f/boot/loader.conf loader rc
162}
163attached_f_passthrough_cleanup()
164{
165	cleanup_root
166}
167
168atf_test_case verbatim_backslash cleanup
169verbatim_backslash_head()
170{
171	atf_set "descr" \
172		"verbatim_ok re-examines the byte after an even \\\\ run"
173}
174verbatim_backslash_body()
175{
176	setup_root
177	printf 'foo=1\n' > "$ROOT/etc/make.conf"
178	# Even-length \\ before # must not skip #: put must reject (Faraz)
179	atf_check -s not-exit:0 -e match:'Invalid argument' \
180	    "$SYSCONF" make -R "$ROOT" 'foo=a\\#b'
181	# A value without a comment marker still writes.
182	atf_check -o match:'foo:' \
183	    "$SYSCONF" make -R "$ROOT" 'foo=a\\b'
184	atf_check -o match:'foo=a\\\\b' cat "$ROOT/etc/make.conf"
185}
186verbatim_backslash_cleanup()
187{
188	cleanup_root
189}
190
191atf_test_case make_strike_last_assign cleanup
192make_strike_last_assign_head()
193{
194	atf_set "descr" \
195		"make -= strikes the last assignment containing the word"
196}
197make_strike_last_assign_body()
198{
199	setup_root
200	printf 'bar+=1\nbar+=1 2\nbar+=1 2 3\n' > "$ROOT/etc/make.conf"
201	atf_check -o match:'->' "$SYSCONF" make -R "$ROOT" 'bar-=3'
202	atf_check -o inline:'bar+=1\nbar+=1 2\nbar+=1 2\n' \
203	    cat "$ROOT/etc/make.conf"
204	atf_check -o match:'->' "$SYSCONF" make -R "$ROOT" 'bar-=2'
205	atf_check -o inline:'bar+=1\nbar+=1 2\nbar+=1\n' \
206	    cat "$ROOT/etc/make.conf"
207	atf_check -o match:'->' "$SYSCONF" make -R "$ROOT" 'bar-=1'
208	atf_check -o inline:'bar+=1\nbar+=1 2\n' cat "$ROOT/etc/make.conf"
209	atf_check -o match:'->' "$SYSCONF" make -R "$ROOT" 'bar-=1'
210	atf_check -o inline:'bar+=1\nbar+=2\n' cat "$ROOT/etc/make.conf"
211	atf_check -o match:'->' "$SYSCONF" make -R "$ROOT" 'bar-=2'
212	atf_check -o inline:'bar+=1\n' cat "$ROOT/etc/make.conf"
213	atf_check -o inline:'bar: 1 (unchanged)\n' \
214	    "$SYSCONF" make -R "$ROOT" 'bar-=9'
215	# Emptying the last word deletes the statement: (removed), like (added)
216	printf 'z+=only\n' >>"$ROOT/etc/make.conf"
217	atf_check -o match:'make.conf:[0-9]*: z\+=only \(removed\)' \
218	    "$SYSCONF" make -R "$ROOT" -V 'z-=only'
219	atf_check -o not-match:'^z\+=' cat "$ROOT/etc/make.conf"
220}
221make_strike_last_assign_cleanup()
222{
223	cleanup_root
224}
225
226atf_test_case equal_value_nocheck_mtime cleanup
227equal_value_nocheck_mtime_head()
228{
229	atf_set "descr" "Equal-value write and -c leave mtime alone"
230}
231equal_value_nocheck_mtime_body()
232{
233	setup_root
234	printf 'bar=1\n' > "$ROOT/etc/make.conf"
235	sleep 1
236	before=$( stat -f '%m' "$ROOT/etc/make.conf" )
237	# Equal-value write: informational echo, file untouched
238	atf_check -o inline:'bar: 1 (unchanged)\n' \
239	    "$SYSCONF" make -R "$ROOT" bar=1
240	# -e uses `=' for write echoes the same as for reads
241	atf_check -o inline:'bar=1 (unchanged)\n' \
242	    "$SYSCONF" make -R "$ROOT" -e bar=1
243	after=$( stat -f '%m' "$ROOT/etc/make.conf" )
244	atf_check_equal "$before" "$after"
245	# -q suppresses the echo; -c is silent and does not write
246	atf_check -o empty "$SYSCONF" make -R "$ROOT" -q bar=1
247	atf_check -o empty "$SYSCONF" make -R "$ROOT" -c bar=1
248	after2=$( stat -f '%m' "$ROOT/etc/make.conf" )
249	atf_check_equal "$before" "$after2"
250	atf_check -o inline:'bar=1 # -> 2\n' \
251	    "$SYSCONF" make -R "$ROOT" -e bar=2
252	atf_check -o inline:'bar=2\n' \
253	    "$SYSCONF" make -R "$ROOT" -e bar
254}
255equal_value_nocheck_mtime_cleanup()
256{
257	cleanup_root
258}
259
260atf_test_case sysctl_oid_range cleanup
261sysctl_oid_range_head()
262{
263	atf_set "descr" \
264		"sysctl target rejects values that overflow the OID CTLTYPE"
265}
266sysctl_oid_range_body()
267{
268	find_sysconf
269	case "$( uname -s )" in
270	FreeBSD) ;;
271	*) atf_skip "requires FreeBSD sysctl OIDFMT" ;;
272	esac
273	# -f alone (no -R) still validates against the running kernel.
274	conf="$( pwd )/sysctl.conf"
275	:> "$conf"
276	atf_check -s exit:1 -e match:'out of range for integer' \
277	    "$SYSCONF" sysctl -f "$conf" \
278	    security.bsd.see_other_uids=99999999999
279	atf_check -o empty cat "$conf"
280	atf_check -s exit:1 -e match:'out of range for unsigned integer' \
281	    "$SYSCONF" sysctl -f "$conf" kern.ipc.somaxconn=-1
282	atf_check -s exit:1 -e match:'out of range for unsigned integer' \
283	    "$SYSCONF" sysctl -f "$conf" kern.ipc.somaxconn=abc
284	atf_check -o match:'see_other_uids' \
285	    "$SYSCONF" sysctl -f "$conf" security.bsd.see_other_uids=0
286	atf_check -o match:'see_other_uids=0' cat "$conf"
287}
288sysctl_oid_range_cleanup()
289{
290	rm -f "$( pwd )/sysctl.conf"
291}
292
293atf_test_case make_src_knobs cleanup
294make_src_knobs_head()
295{
296	atf_set "descr" \
297		"-s WITH_/WITHOUT_ presence, query display, unchanged, -x"
298}
299make_src_knobs_body()
300{
301	setup_root
302	:> "$ROOT/etc/make.conf"
303	:> "$ROOT/etc/sysctl.conf"
304	:> "$ROOT/etc/src-env.conf"
305	:> "$ROOT/etc/src.conf"
306
307	# -s only on make/src
308	atf_check -s exit:1 -e match:'only valid for make and src' \
309	    "$SYSCONF" sysctl -R "$ROOT" -s WITHOUT_FOO
310	# name must be WITH_/WITHOUT_
311	atf_check -s exit:1 -e match:'WITH_ or WITHOUT_' \
312	    "$SYSCONF" make -R "$ROOT" -s CFLAGS
313	# no value with -s
314	atf_check -s exit:1 -e match:'does not take a value' \
315	    "$SYSCONF" make -R "$ROOT" -s 'WITHOUT_FOO='
316
317	atf_check -o inline:'WITHOUT_FOO: (not present) -> (present)\n' \
318	    "$SYSCONF" make -R "$ROOT" -s WITHOUT_FOO
319	atf_check -o match:'^WITHOUT_FOO=$' cat "$ROOT/etc/make.conf"
320	atf_check -o inline:'WITHOUT_FOO (present)\n' \
321	    "$SYSCONF" make -R "$ROOT" WITHOUT_FOO
322	atf_check -o inline:'WITHOUT_FOO=\n' \
323	    "$SYSCONF" make -R "$ROOT" -e WITHOUT_FOO
324	atf_check -o inline:'\n' \
325	    "$SYSCONF" make -R "$ROOT" -n WITHOUT_FOO
326
327	before=$( stat -f '%m' "$ROOT/etc/make.conf" )
328	atf_check -o inline:'WITHOUT_FOO: (present) (unchanged)\n' \
329	    "$SYSCONF" make -R "$ROOT" -s WITHOUT_FOO
330	after=$( stat -f '%m' "$ROOT/etc/make.conf" )
331	atf_check_equal "$before" "$after"
332
333	# Non-empty placeholder (=t) still means present (defined())
334	atf_check -o inline:'WITHOUT_BAR: (not present) -> (present)\n' \
335	    "$SYSCONF" make -R "$ROOT" 'WITHOUT_BAR=t'
336	atf_check -o match:'^WITHOUT_BAR=t$' cat "$ROOT/etc/make.conf"
337	atf_check -o inline:'WITHOUT_BAR (present)\n' \
338	    "$SYSCONF" make -R "$ROOT" WITHOUT_BAR
339	atf_check -o inline:'WITHOUT_BAR=t\n' \
340	    "$SYSCONF" make -R "$ROOT" -e WITHOUT_BAR
341	atf_check -o inline:'t\n' \
342	    "$SYSCONF" make -R "$ROOT" -n WITHOUT_BAR
343	before=$( stat -f '%m' "$ROOT/etc/make.conf" )
344	atf_check -o inline:'WITHOUT_BAR: (present) (unchanged)\n' \
345	    "$SYSCONF" make -R "$ROOT" -s WITHOUT_BAR
346	after=$( stat -f '%m' "$ROOT/etc/make.conf" )
347	atf_check_equal "$before" "$after"
348	atf_check -o match:'^WITHOUT_BAR=t$' cat "$ROOT/etc/make.conf"
349
350	# Explicit name= always writes (incl. empty); echo value changes
351	atf_check -o inline:'WITHOUT_BAR: t -> \n' \
352	    "$SYSCONF" make -R "$ROOT" 'WITHOUT_BAR='
353	atf_check -o match:'^WITHOUT_BAR=$' cat "$ROOT/etc/make.conf"
354	atf_check -o inline:'WITHOUT_BAR:  -> 123\n' \
355	    "$SYSCONF" make -R "$ROOT" 'WITHOUT_BAR=123'
356	atf_check -o match:'^WITHOUT_BAR=123$' cat "$ROOT/etc/make.conf"
357	atf_check -o inline:'WITHOUT_BAR: 123 -> 456\n' \
358	    "$SYSCONF" make -R "$ROOT" 'WITHOUT_BAR=456'
359	atf_check -o match:'^WITHOUT_BAR=456$' cat "$ROOT/etc/make.conf"
360	# -s still no-op while present with a value
361	before=$( stat -f '%m' "$ROOT/etc/make.conf" )
362	atf_check -o inline:'WITHOUT_BAR: (present) (unchanged)\n' \
363	    "$SYSCONF" make -R "$ROOT" -s WITHOUT_BAR
364	after=$( stat -f '%m' "$ROOT/etc/make.conf" )
365	atf_check_equal "$before" "$after"
366	atf_check -o match:'^WITHOUT_BAR=456$' cat "$ROOT/etc/make.conf"
367
368	# src prefers src.conf
369	atf_check -o inline:'WITHOUT_LLDB: (not present) -> (present)\n' \
370	    "$SYSCONF" src -R "$ROOT" -s WITHOUT_LLDB
371	atf_check -o match:'^WITHOUT_LLDB=$' cat "$ROOT/etc/src.conf"
372	# -x is quiet unless -v/-V (same as non-knob removals)
373	atf_check -o empty \
374	    "$SYSCONF" src -R "$ROOT" -x WITHOUT_LLDB
375	atf_check -o empty cat "$ROOT/etc/src.conf"
376	atf_check -o inline:'WITHOUT_LLDB: (not present) -> (present)\n' \
377	    "$SYSCONF" src -R "$ROOT" -s WITHOUT_LLDB
378	atf_check -o match:'WITHOUT_LLDB \(removed\)' \
379	    "$SYSCONF" src -R "$ROOT" -vx WITHOUT_LLDB
380	atf_check -o empty cat "$ROOT/etc/src.conf"
381
382	# WITH_*=no (exact lowercase) warns like bsd.mkopt.mk; WITHOUT_*=no
383	# and WITH_*=NO do not
384	atf_check -e match:'Use WITHOUT_FOO=1 instead of WITH_FOO=no' \
385	    -o match:'WITH_FOO' \
386	    "$SYSCONF" make -R "$ROOT" 'WITH_FOO=no'
387	atf_check -o match:'^WITH_FOO=no$' cat "$ROOT/etc/make.conf"
388	atf_check -e empty -o match:'WITH_BAR' \
389	    "$SYSCONF" make -R "$ROOT" 'WITH_BAR=NO'
390	atf_check -e empty -o match:'WITHOUT_BAZ' \
391	    "$SYSCONF" make -R "$ROOT" 'WITHOUT_BAZ=no'
392
393	# Reads remind with file:line (presence display hides =no)
394	atf_check -e match:'make.conf:[0-9]+: Use WITHOUT_FOO=1 instead of WITH_FOO=no' \
395	    -o inline:'WITH_FOO (present)\n' \
396	    "$SYSCONF" make -R "$ROOT" WITH_FOO
397	atf_check -e match:'make.conf:[0-9]+: Use WITHOUT_FOO=1 instead of WITH_FOO=no' \
398	    -o match:'WITH_FOO \(present\)' \
399	    "$SYSCONF" make -R "$ROOT" -a
400	atf_check -e match:'make.conf:[0-9]+: Use WITHOUT_FOO=1 instead of WITH_FOO=no' \
401	    -o inline:'WITH_FOO=no\n' \
402	    "$SYSCONF" make -R "$ROOT" -e WITH_FOO
403	atf_check -e empty -o inline:'WITH_FOO (present)\n' \
404	    "$SYSCONF" make -R "$ROOT" -q WITH_FOO
405
406	# WITHOUT_MODULES is not a presence knob
407	atf_check -s exit:1 -e match:'WITHOUT_MODULES takes a module list' \
408	    "$SYSCONF" make -R "$ROOT" -s WITHOUT_MODULES
409	atf_check -o match:'WITHOUT_MODULES:  -> plip' \
410	    "$SYSCONF" make -R "$ROOT" 'WITHOUT_MODULES=plip'
411	atf_check -o inline:'WITHOUT_MODULES: plip\n' \
412	    "$SYSCONF" make -R "$ROOT" WITHOUT_MODULES
413}
414make_src_knobs_cleanup()
415{
416	cleanup_root
417}
418
419atf_test_case max_bytes_cap cleanup
420max_bytes_cap_head()
421{
422	atf_set "descr" "BSDCONF_MAX_BYTES bounds parse input"
423}
424max_bytes_cap_body()
425{
426	setup_root
427	printf 'foo=ok\n' > "$ROOT/etc/make.conf"
428	atf_check -o inline:'foo: ok\n' "$SYSCONF" make -R "$ROOT" foo
429	# A tight cap rejects an otherwise valid file
430	atf_check -s not-exit:0 -e match:'File too large' \
431	    env BSDCONF_MAX_BYTES=4 "$SYSCONF" make -R "$ROOT" foo
432	got=$( printf 'bar=piped\n' | "$SYSCONF" generic -f - bar ) ||
433		atf_fail "pipe parse failed"
434	atf_check_equal "bar: piped" "$got"
435}
436max_bytes_cap_cleanup()
437{
438	cleanup_root
439}
440
441atf_init_test_cases()
442{
443	atf_add_test_case make_src_knobs
444	atf_add_test_case make_append_accumulate
445	atf_add_test_case make_strike_last_assign
446	atf_add_test_case src_triad_and_trail
447	atf_add_test_case src_env_rejected
448	atf_add_test_case env_overrides
449	atf_add_test_case attached_f_passthrough
450	atf_add_test_case verbatim_backslash
451	atf_add_test_case equal_value_nocheck_mtime
452	atf_add_test_case sysctl_oid_range
453	atf_add_test_case max_bytes_cap
454}
455