xref: /freebsd/contrib/bmake/unit-tests/varmod-edge.mk (revision 1edb7116f450c1a1793f2fd25f6bdc16735ef888)
1# $NetBSD: varmod-edge.mk,v 1.19 2023/11/19 22:06:15 rillig Exp $
2#
3# Tests for edge cases in variable modifiers.
4#
5# These tests demonstrate the current implementation in small examples.
6# They may contain surprising behavior.
7#
8# Each test consists of:
9# - INP, the input to the test
10# - MOD, the expression for testing the modifier
11# - EXP, the expected output
12
13TESTS+=		M-paren
14INP.M-paren=	(parentheses) {braces} (opening closing) ()
15MOD.M-paren=	${INP.M-paren:M(*)}
16EXP.M-paren=	(parentheses) ()
17
18# The first closing brace matches the opening parenthesis.
19# The second closing brace actually ends the expression.
20#
21# XXX: This is unexpected but rarely occurs in practice.
22TESTS+=		M-mixed
23INP.M-mixed=	(paren-brace} (
24MOD.M-mixed=	${INP.M-mixed:M(*}}
25EXP.M-mixed=	(paren-brace}
26
27# After the :M modifier has parsed the pattern, only the closing brace
28# and the colon are unescaped. The other characters are left as-is.
29# To actually see this effect, the backslashes in the :M modifier need
30# to be doubled since single backslashes would simply be unescaped by
31# Str_Match.
32#
33# XXX: This is unexpected. The opening brace should also be unescaped.
34TESTS+=		M-unescape
35INP.M-unescape=	({}): \(\{\}\)\: \(\{}\):
36MOD.M-unescape=	${INP.M-unescape:M\\(\\{\\}\\)\\:}
37EXP.M-unescape=	\(\{}\):
38
39# When the :M and :N modifiers are parsed, the pattern finishes as soon
40# as open_parens + open_braces == closing_parens + closing_braces. This
41# means that ( and } form a matching pair.
42#
43# Nested expressions are not parsed as such. Instead, only the
44# parentheses and braces are counted. This leads to a parse error since
45# the nested expression is not "${:U*)}" but only "${:U*)", which is
46# missing the closing brace. The expression is evaluated anyway.
47# The final brace in the output comes from the end of M.nest-mix.
48#
49# XXX: This is unexpected but rarely occurs in practice.
50TESTS+=		M-nest-mix
51INP.M-nest-mix=	(parentheses)
52MOD.M-nest-mix=	${INP.M-nest-mix:M${:U*)}}
53EXP.M-nest-mix=	(parentheses)}
54# make: Unclosed expression, expecting '}' for modifier "U*)" of variable "" with value "*)"
55
56# In contrast to parentheses and braces, the brackets are not counted
57# when the :M modifier is parsed since Makefile variables only take the
58# ${VAR} or $(VAR) forms, but not $[VAR].
59#
60# The final ] in the pattern is needed to close the character class.
61TESTS+=		M-nest-brk
62INP.M-nest-brk=	[ [[ [[[
63MOD.M-nest-brk=	${INP.M-nest-brk:M${:U[[[[[]}}
64EXP.M-nest-brk=	[
65
66# The pattern in the nested variable has an unclosed character class.
67# No error is reported though, and the pattern is closed implicitly.
68#
69# XXX: It is unexpected that no error is reported.
70# See str.c, function Str_Match.
71#
72# Before 2019-12-02, this test case triggered an out-of-bounds read
73# in Str_Match.
74TESTS+=		M-pat-err
75INP.M-pat-err=	[ [[ [[[
76MOD.M-pat-err=	${INP.M-pat-err:M${:U[[}}
77EXP.M-pat-err=	[
78
79# The first backslash does not escape the second backslash.
80# Therefore, the second backslash escapes the parenthesis.
81# This means that the pattern ends there.
82# The final } in the output comes from the end of MOD.M-bsbs.
83#
84# If the first backslash were to escape the second backslash, the first
85# closing brace would match the opening parenthesis (see M-mixed), and
86# the second closing brace would be needed to close the variable.
87# After that, the remaining backslash would escape the parenthesis in
88# the pattern, therefore (} would match.
89TESTS+=		M-bsbs
90INP.M-bsbs=	(} \( \(}
91MOD.M-bsbs=	${INP.M-bsbs:M\\(}}
92EXP.M-bsbs=	\(}
93#EXP.M-bsbs=	(}	# If the first backslash were to escape ...
94
95# The backslash in \( does not escape the parenthesis, therefore it
96# counts for the nesting level and matches with the first closing brace.
97# The second closing brace closes the variable, and the third is copied
98# literally.
99#
100# The second :M in the pattern is nested between ( and }, therefore it
101# does not start a new modifier.
102TESTS+=		M-bs1-par
103INP.M-bs1-par=	( (:M (:M} \( \(:M \(:M}
104MOD.M-bs1-par=	${INP.M-bs1-par:M\(:M*}}}
105EXP.M-bs1-par=	(:M}}
106
107# The double backslash is passed verbatim to the pattern matcher.
108# The Str_Match pattern is \\(:M*}, and there the backslash is unescaped.
109# Again, the ( takes place in the nesting level, and there is no way to
110# prevent this, no matter how many backslashes are used.
111TESTS+=		M-bs2-par
112INP.M-bs2-par=	( (:M (:M} \( \(:M \(:M}
113MOD.M-bs2-par=	${INP.M-bs2-par:M\\(:M*}}}
114EXP.M-bs2-par=	\(:M}}
115
116# Str_Match uses a recursive algorithm for matching the * patterns.
117# Make sure that it survives patterns with 128 asterisks.
118# That should be enough for all practical purposes.
119# To produce a stack overflow, just add more :Qs below.
120TESTS+=		M-128
121INP.M-128=	${:U\\:Q:Q:Q:Q:Q:Q:Q:S,\\,x,g}
122PAT.M-128=	${:U\\:Q:Q:Q:Q:Q:Q:Q:S,\\,*,g}
123MOD.M-128=	${INP.M-128:M${PAT.M-128}}
124EXP.M-128=	${INP.M-128}
125
126# This is the normal SysV substitution. Nothing surprising here.
127TESTS+=		eq-ext
128INP.eq-ext=	file.c file.cc
129MOD.eq-ext=	${INP.eq-ext:%.c=%.o}
130EXP.eq-ext=	file.o file.cc
131
132# The SysV := modifier is greedy and consumes all the modifier text
133# up until the closing brace or parenthesis. The :Q may look like a
134# modifier, but it really isn't, that's why it appears in the output.
135TESTS+=		eq-q
136INP.eq-q=	file.c file.cc
137MOD.eq-q=	${INP.eq-q:%.c=%.o:Q}
138EXP.eq-q=	file.o:Q file.cc
139
140# The = in the := modifier can be escaped.
141TESTS+=		eq-bs
142INP.eq-bs=	file.c file.c=%.o
143MOD.eq-bs=	${INP.eq-bs:%.c\=%.o=%.ext}
144EXP.eq-bs=	file.c file.ext
145
146# Having only an escaped '=' results in a parse error.
147# The call to "pattern.lhs = ParseModifierPart" fails.
148TESTS+=		eq-esc
149INP.eq-esc=	file.c file...
150MOD.eq-esc=	${INP.eq-esc:a\=b}
151EXP.eq-esc=	# empty
152# make: Unfinished modifier for INP.eq-esc ('=' missing)
153
154TESTS+=		colon
155INP.colon=	value
156MOD.colon=	${INP.colon:}
157EXP.colon=	value
158
159TESTS+=		colons
160INP.colons=	value
161MOD.colons=	${INP.colons::::}
162EXP.colons=	# empty
163
164.for test in ${TESTS}
165# expect+2: Unknown modifier ":"
166# expect+1: Unknown modifier ":"
167.  if ${MOD.${test}} == ${EXP.${test}}
168# expect+16: ok M-paren
169# expect+15: ok M-mixed
170# expect+14: ok M-unescape
171# expect+13: ok M-nest-mix
172# expect+12: ok M-nest-brk
173# expect+11: ok M-pat-err
174# expect+10: ok M-bsbs
175# expect+09: ok M-bs1-par
176# expect+08: ok M-bs2-par
177# expect+07: ok M-128
178# expect+06: ok eq-ext
179# expect+05: ok eq-q
180# expect+04: ok eq-bs
181# expect+03: ok eq-esc
182# expect+02: ok colon
183# expect+01: ok colons
184.    info ok ${test}
185.  else
186.    warning error in ${test}: expected "${EXP.${test}}", got "${MOD.${test}}"
187.  endif
188.endfor
189
190# Even in expressions based on an unnamed variable, there may be errors.
191# XXX: The error message should mention the variable name of the expression,
192# even though that name is empty in this case.
193# expect+2: Malformed conditional (${:Z})
194# expect+1: Unknown modifier "Z"
195.if ${:Z}
196.  error
197.else
198.  error
199.endif
200
201# Even in expressions based on an unnamed variable, there may be errors.
202#
203# Before var.c 1.842 from 2021-02-23, the error message did not surround the
204# variable name with quotes, leading to the rather confusing "Unfinished
205# modifier for  (',' missing)", having two spaces in a row.
206#
207# XXX: The error message should report the filename:lineno.
208# expect+1: Malformed conditional (${:S,})
209.if ${:S,}
210.  error
211.else
212.  error
213.endif
214
215all:
216	@echo ok
217