summaryrefslogtreecommitdiff
path: root/unit-tests
diff options
context:
space:
mode:
Diffstat (limited to 'unit-tests')
-rw-r--r--unit-tests/Makefile (renamed from unit-tests/Makefile.in)19
-rw-r--r--unit-tests/Makefile.config.in4
-rw-r--r--unit-tests/cond-late.exp3
-rw-r--r--unit-tests/cond-late.mk23
-rw-r--r--unit-tests/dollar.exp51
-rw-r--r--unit-tests/dollar.mk81
-rw-r--r--unit-tests/escape.mk6
-rw-r--r--unit-tests/forloop.exp3
-rw-r--r--unit-tests/forloop.mk12
-rw-r--r--unit-tests/include-main.exp6
-rw-r--r--unit-tests/include-main.mk30
-rw-r--r--unit-tests/include-sub.mk17
-rw-r--r--unit-tests/include-subsub.mk7
-rw-r--r--unit-tests/modorder.mk7
-rw-r--r--unit-tests/sysv.exp8
-rw-r--r--unit-tests/sysv.mk21
-rw-r--r--unit-tests/varmod-edge.exp17
-rw-r--r--unit-tests/varmod-edge.mk162
18 files changed, 458 insertions, 19 deletions
diff --git a/unit-tests/Makefile.in b/unit-tests/Makefile
index e51d25455a50..5cf0212ce596 100644
--- a/unit-tests/Makefile.in
+++ b/unit-tests/Makefile
@@ -1,6 +1,6 @@
-# $Id: Makefile.in,v 1.49 2018/09/21 21:39:05 sjg Exp $
+# $Id: Makefile,v 1.54 2020/05/17 17:26:14 sjg Exp $
#
-# $NetBSD: Makefile,v 1.53 2018/05/24 00:25:44 christos Exp $
+# $NetBSD: Makefile,v 1.58 2020/05/17 12:36:26 rillig Exp $
#
# Unit tests for make(1)
# The main targets are:
@@ -15,28 +15,31 @@
# and it should be added to the TESTNAMES list.
#
-srcdir= @srcdir@
-
.MAIN: all
-UNIT_TESTS:= ${srcdir}
+.-include "Makefile.config"
+
+UNIT_TESTS:= ${.PARSEDIR}
.PATH: ${UNIT_TESTS}
# Each test is in a sub-makefile.
# Keep the list sorted.
TESTNAMES= \
comment \
+ cond-late \
cond1 \
cond2 \
+ dollar \
+ doterror \
+ dotwait \
error \
export \
export-all \
export-env \
- doterror \
- dotwait \
forloop \
forsubst \
hash \
+ include-main \
misc \
moderrs \
modmatch \
@@ -54,6 +57,7 @@ TESTNAMES= \
unexport-env \
varcmd \
varmisc \
+ varmod-edge \
varquote \
varshell
@@ -88,7 +92,6 @@ TEST_MAKE?= ${.MAKE}
TOOL_SED?= sed
TOOL_TR?= tr
TOOL_DIFF?= diff
-DIFF_FLAGS?= @diff_u@
.if defined(.PARSEDIR)
# ensure consistent results from sort(1)
diff --git a/unit-tests/Makefile.config.in b/unit-tests/Makefile.config.in
new file mode 100644
index 000000000000..0fe24f08d2f9
--- /dev/null
+++ b/unit-tests/Makefile.config.in
@@ -0,0 +1,4 @@
+# $Id: Makefile.config.in,v 1.1 2018/12/30 17:14:24 sjg Exp $
+
+srcdir= @srcdir@
+DIFF_FLAGS?= @diff_u@
diff --git a/unit-tests/cond-late.exp b/unit-tests/cond-late.exp
new file mode 100644
index 000000000000..3717b088c646
--- /dev/null
+++ b/unit-tests/cond-late.exp
@@ -0,0 +1,3 @@
+yes
+no
+exit status 0
diff --git a/unit-tests/cond-late.mk b/unit-tests/cond-late.mk
new file mode 100644
index 000000000000..2eb02b42b920
--- /dev/null
+++ b/unit-tests/cond-late.mk
@@ -0,0 +1,23 @@
+# $NetBSD: cond-late.mk,v 1.1 2020/04/29 23:15:21 rillig Exp $
+#
+# Using the :? modifier, variable expressions can contain conditional
+# expressions that are evaluated late. Any variables appearing in these
+# conditions are expanded before parsing the condition. This is
+# different from many other places.
+#
+# Because of this, variables that are used in these lazy conditions
+# should not contain double-quotes, or the parser will probably fail.
+#
+# They should also not contain operators like == or <, since these are
+# actually interpreted as these operators. This is demonstrated below.
+#
+# If the order of evaluation were to change to first parse the condition
+# and then expand the variables, the output would change from the
+# current "yes no" to "yes yes", since both variables are non-empty.
+
+COND.true= "yes" == "yes"
+COND.false= "yes" != "yes"
+
+all:
+ @echo ${ ${COND.true} :?yes:no}
+ @echo ${ ${COND.false} :?yes:no}
diff --git a/unit-tests/dollar.exp b/unit-tests/dollar.exp
new file mode 100644
index 000000000000..496adc02f157
--- /dev/null
+++ b/unit-tests/dollar.exp
@@ -0,0 +1,51 @@
+
+Printing dollar from literals and variables
+
+To survive the parser, a dollar character must be doubled.
+ 1 dollar literal => <single-quote-var-value>
+ 1 dollar literal eol => <>
+ 2 dollar literal => <$>
+ 4 dollar literal => <$$>
+Some hungry part of make eats all the dollars after a :U modifier.
+ 1 dollar default => <>
+ 2 dollar default => <>
+ 4 dollar default => <>
+This works as expected.
+ 1 dollar variable => <>
+ 2 dollar variable => <$>
+ 4 dollar variable => <$$>
+Some hungry part of make eats all the dollars after a :U modifier.
+ 1 dollar var-default => <>
+ 2 dollar var-default => <$>
+ 4 dollar var-default => <$$>
+
+Dollar in :S pattern
+
+ S,$,word, => <$XYword>
+ S,$X,word, => <$XY>
+ S,$$X,word, => <$XY>
+ S,$$$X,word, => <$XY>
+ S,$X,replaced, => <replaced>
+ S,$$X,replaced, => <replaced>
+ S,$$$X,replaced, => <replaced>
+
+Dollar in :C character class
+
+The A is replaced because the $$ is reduced to a single $,
+which is then resolved to the variable X with the value VAR_X.
+The effective character class becomes [VAR_XY].
+ C,[$$XY],<&>,g => <$<A><X><Y>>
+
+Dollar in :C pattern
+
+For some reason, multiple dollars are folded into one.
+ C,$,dollar,g => <>
+ C,$$,dollar,g => <>
+
+Dollar in :S replacement
+
+For some reason, multiple dollars are folded into one.
+ S,word,a$Xo, => <aVAR_Xo>
+ S,word,a$$Xo, => <aVAR_Xo>
+ S,word,a$$$Xo, => <aVAR_Xo>
+exit status 0
diff --git a/unit-tests/dollar.mk b/unit-tests/dollar.mk
new file mode 100644
index 000000000000..b7f7d9fa7c9e
--- /dev/null
+++ b/unit-tests/dollar.mk
@@ -0,0 +1,81 @@
+# $NetBSD: dollar.mk,v 1.3 2020/05/17 09:37:48 rillig Exp $
+#
+# Test the various places where a dollar character can appear and
+# see what happens. There are lots of surprises here.
+#
+
+LIST= plain 'single' "double" 'mix'"ed" back\ slashed
+WORD= word
+
+DOLLAR1= $
+DOLLAR2= $$
+DOLLAR4= $$$$
+
+X= VAR_X
+DOLLAR_XY= $$XY
+DOLLAR_AXY= $$AXY
+
+H= @header() { printf '\n%s\n\n' "$$*"; }; header
+T= @testcase() { printf '%23s => <%s>\n' "$$@"; }; testcase
+C= @comment() { printf '%s\n' "$$*"; }; comment
+
+# These variable values are not accessed.
+# The trailing dollar in the '1 dollar literal eol' test case accesses
+# the empty variable instead, which is always guaranteed to be empty.
+${:U }= space-var-value
+${:U${.newline}}= newline-var-value
+# But this one is accessed.
+${:U'}= single-quote-var-value'
+
+all:
+ $H 'Printing dollar from literals and variables'
+
+ $C 'To survive the parser, a dollar character must be doubled.'
+ $T '1 dollar literal' '$'
+ $T '1 dollar literal eol' ''$
+ $T '2 dollar literal' '$$'
+ $T '4 dollar literal' '$$$$'
+
+ $C 'Some hungry part of make eats all the dollars after a :U modifier.'
+ $T '1 dollar default' ''${:U$:Q}
+ $T '2 dollar default' ''${:U$$:Q}
+ $T '4 dollar default' ''${:U$$$$:Q}
+
+ $C 'This works as expected.'
+ $T '1 dollar variable' ''${DOLLAR1:Q}
+ $T '2 dollar variable' ''${DOLLAR2:Q}
+ $T '4 dollar variable' ''${DOLLAR4:Q}
+
+ $C 'Some hungry part of make eats all the dollars after a :U modifier.'
+ $T '1 dollar var-default' ''${:U${DOLLAR1}:Q}
+ $T '2 dollar var-default' ''${:U${DOLLAR2}:Q}
+ $T '4 dollar var-default' ''${:U${DOLLAR4}:Q}
+
+ $H 'Dollar in :S pattern'
+
+ $T 'S,$$,word,' ''${DOLLAR_XY:S,$,word,:Q}
+ $T 'S,$$X,word,' ''${DOLLAR_XY:S,$X,word,:Q}
+ $T 'S,$$$$X,word,' ''${DOLLAR_XY:S,$$X,word,:Q}
+ $T 'S,$$$$$$X,word,' ''${DOLLAR_XY:S,$$$X,word,:Q}
+
+ $T 'S,$$X,replaced,' ''${X:S,$X,replaced,:Q}
+ $T 'S,$$$$X,replaced,' ''${X:S,$$X,replaced,:Q}
+ $T 'S,$$$$$$X,replaced,' ''${X:S,$$$X,replaced,:Q}
+
+ $H 'Dollar in :C character class'
+
+ $C 'The A is replaced because the $$$$ is reduced to a single $$,'
+ $C 'which is then resolved to the variable X with the value VAR_X.'
+ $C 'The effective character class becomes [VAR_XY].'
+ $T 'C,[$$$$XY],<&>,g' ''${DOLLAR_AXY:C,[$$XY],<&>,g:Q}
+
+ $H 'Dollar in :C pattern'
+ $C 'For some reason, multiple dollars are folded into one.'
+ $T 'C,$$,dollar,g' ''${DOLLAR:C,$,dollar,g:Q}
+ $T 'C,$$$$,dollar,g' ''${DOLLAR:C,$$,dollar,g:Q}
+
+ $H 'Dollar in :S replacement'
+ $C 'For some reason, multiple dollars are folded into one.'
+ $T 'S,word,a$$Xo,' ''${WORD:S,word,a$Xo,:Q}
+ $T 'S,word,a$$$$Xo,' ''${WORD:S,word,a$$Xo,:Q}
+ $T 'S,word,a$$$$$$Xo,' ''${WORD:S,word,a$$$Xo,:Q}
diff --git a/unit-tests/escape.mk b/unit-tests/escape.mk
index bb37c9217f4c..be9c4dd60e3f 100644
--- a/unit-tests/escape.mk
+++ b/unit-tests/escape.mk
@@ -1,4 +1,4 @@
-# $Id: escape.mk,v 1.1.1.2 2014/11/06 01:40:37 sjg Exp $
+# $Id: escape.mk,v 1.1.1.3 2020/01/22 01:07:14 sjg Exp $
#
# Test backslash escaping.
@@ -35,8 +35,8 @@
# Also, our practice is that an even number of backslashes before a
# newline in a variable assignment simply stores the backslashes as part
# of the value, and treats the newline as though it was not escaped.
-# Similarly, ann even number of backslashes before a newline in a
-# command simply uses the backslashes as part of the command test, but
+# Similarly, an even number of backslashes before a newline in a
+# command simply uses the backslashes as part of the command, but
# does not escape the newline. This is compatible with GNU make.
all: .PHONY
diff --git a/unit-tests/forloop.exp b/unit-tests/forloop.exp
index df14b751224c..998456190737 100644
--- a/unit-tests/forloop.exp
+++ b/unit-tests/forloop.exp
@@ -7,12 +7,13 @@ x=-I"This or that"
x=-Ithat
x="-DTHIS=\"this and that\""
cfl=-I/this -I"This or that" -Ithat "-DTHIS=\"this and that\""
+newline-item=(a)
a=one b="two and three"
a=four b="five"
a=ONE b="TWO AND THREE"
a=FOUR b="FIVE"
We expect an error next:
-make: "forloop.mk" line 38: Wrong number of words (9) in .for substitution list with 2 vars
+make: "forloop.mk" line 46: Wrong number of words (9) in .for substitution list with 2 vars
make: Fatal errors encountered -- cannot continue
make: stopped in unit-tests
OK
diff --git a/unit-tests/forloop.mk b/unit-tests/forloop.mk
index 9aad7c6b998a..dd3f07ddb19a 100644
--- a/unit-tests/forloop.mk
+++ b/unit-tests/forloop.mk
@@ -1,4 +1,4 @@
-# $Id: forloop.mk,v 1.1.1.1 2014/08/30 18:57:18 sjg Exp $
+# $Id: forloop.mk,v 1.1.1.2 2020/05/05 00:54:40 sjg Exp $
all: for-loop
@@ -33,7 +33,15 @@ X!= echo 'cfl=${cfl}' >&2; echo
.for a b in ${EMPTY}
X!= echo 'a=$a b=$b' >&2; echo
.endfor
-.endif
+
+# Since at least 1993, iteration stops at the first newline.
+# Back then, the .newline variable didn't exist, therefore it was unlikely
+# that a newline ever occured.
+.for var in a${.newline}b${.newline}c
+X!= echo 'newline-item=('${var:Q}')' 1>&2; echo
+.endfor
+
+.endif # for-fail
.for a b in ${LIST} ${LIST:tu} ${XTRA_LIST}
X!= echo 'a=$a b=$b' >&2; echo
diff --git a/unit-tests/include-main.exp b/unit-tests/include-main.exp
new file mode 100644
index 000000000000..7a55c6e97dce
--- /dev/null
+++ b/unit-tests/include-main.exp
@@ -0,0 +1,6 @@
+main-before-ok
+sub-before-ok
+subsub-ok
+sub-after-fail(include-sub.mk)
+main-after-fail(include-sub.mk)
+exit status 0
diff --git a/unit-tests/include-main.mk b/unit-tests/include-main.mk
new file mode 100644
index 000000000000..452b6102a5ce
--- /dev/null
+++ b/unit-tests/include-main.mk
@@ -0,0 +1,30 @@
+# $NetBSD: include-main.mk,v 1.1 2020/05/17 12:36:26 rillig Exp $
+#
+# Demonstrates that the .INCLUDEDFROMFILE magic variable does not behave
+# as described in the manual page.
+#
+# The manual page says that it is the "filename of the file this Makefile
+# was included from", while in reality it is the "filename in which the
+# latest .include happened".
+#
+
+.if !defined(.INCLUDEDFROMFILE)
+LOG+= main-before-ok
+.else
+. for f in ${.INCLUDEDFROMFILE}
+LOG+= main-before-fail\(${f:Q}\)
+. endfor
+.endif
+
+.include "include-sub.mk"
+
+.if !defined(.INCLUDEDFROMFILE)
+LOG+= main-after-ok
+.else
+. for f in ${.INCLUDEDFROMFILE}
+LOG+= main-after-fail\(${f:Q}\)
+. endfor
+.endif
+
+all:
+ @printf '%s\n' ${LOG}
diff --git a/unit-tests/include-sub.mk b/unit-tests/include-sub.mk
new file mode 100644
index 000000000000..28856fcc6e60
--- /dev/null
+++ b/unit-tests/include-sub.mk
@@ -0,0 +1,17 @@
+# $NetBSD: include-sub.mk,v 1.1 2020/05/17 12:36:26 rillig Exp $
+
+.if ${.INCLUDEDFROMFILE} == "include-main.mk"
+LOG+= sub-before-ok
+.else
+LOG+= sub-before-fail
+.endif
+
+.include "include-subsub.mk"
+
+.if ${.INCLUDEDFROMFILE} == "include-main.mk"
+LOG+= sub-after-ok
+.else
+. for f in ${.INCLUDEDFROMFILE}
+LOG+= sub-after-fail\(${f:Q}\)
+. endfor
+.endif
diff --git a/unit-tests/include-subsub.mk b/unit-tests/include-subsub.mk
new file mode 100644
index 000000000000..e8fa1186f41d
--- /dev/null
+++ b/unit-tests/include-subsub.mk
@@ -0,0 +1,7 @@
+# $NetBSD: include-subsub.mk,v 1.1 2020/05/17 12:36:26 rillig Exp $
+
+.if ${.INCLUDEDFROMFILE:T} == "include-sub.mk"
+LOG+= subsub-ok
+.else
+LOG+= subsub-fail
+.endif
diff --git a/unit-tests/modorder.mk b/unit-tests/modorder.mk
index bc24d339f45a..0f809391712f 100644
--- a/unit-tests/modorder.mk
+++ b/unit-tests/modorder.mk
@@ -1,4 +1,4 @@
-# $NetBSD: modorder.mk,v 1.1 2014/08/21 13:44:51 apb Exp $
+# $NetBSD: modorder.mk,v 1.2 2020/01/07 22:42:14 rillig Exp $
LIST= one two three four five six seven eight nine ten
LISTX= ${LIST:Ox}
@@ -12,8 +12,9 @@ all:
@echo "LIST:O = ${LIST:O}"
# Note that 1 in every 10! trials two independently generated
# randomized orderings will be the same. The test framework doesn't
- # support checking probabilistic output, so we accept that the test
- # will incorrectly fail with probability 2.8E-7.
+ # support checking probabilistic output, so we accept that each of the
+ # 3 :Ox tests will incorrectly fail with probability 2.756E-7, which
+ # lets the whole test fail once in 1.209.600 runs, on average.
@echo "LIST:Ox = `test '${LIST:Ox}' != '${LIST:Ox}' ${TEST_RESULT}`"
@echo "LIST:O:Ox = `test '${LIST:O:Ox}' != '${LIST:O:Ox}' ${TEST_RESULT}`"
@echo "LISTX = `test '${LISTX}' != '${LISTX}' ${TEST_RESULT}`"
diff --git a/unit-tests/sysv.exp b/unit-tests/sysv.exp
index 4cce2de31918..610f97c39e85 100644
--- a/unit-tests/sysv.exp
+++ b/unit-tests/sysv.exp
@@ -4,4 +4,12 @@ fun
fun
fun
In the Sun
+acme
+aam.d
+sam.c
+a%.c
+asam.c.c
+asam.c
+a.c.c
+
exit status 0
diff --git a/unit-tests/sysv.mk b/unit-tests/sysv.mk
index d5e99ff950ae..2bd128eb7130 100644
--- a/unit-tests/sysv.mk
+++ b/unit-tests/sysv.mk
@@ -1,4 +1,4 @@
-# $Id: sysv.mk,v 1.2 2014/08/30 22:25:14 sjg Exp $
+# $Id: sysv.mk,v 1.4 2020/05/07 01:17:51 sjg Exp $
FOO ?=
FOOBAR = ${FOO:=bar}
@@ -11,7 +11,7 @@ FUN = ${B}${S}fun
SUN = the Sun
# we expect nothing when FOO is empty
-all: foo fun
+all: foo fun sam bla
foo:
@echo FOOBAR = ${FOOBAR}
@@ -24,3 +24,20 @@ fun:
@echo ${FUN:${B}${S}fun=fun}
@echo ${FUN:${B}${S}%=%}
@echo ${In:L:%=% ${SUN}}
+
+
+SAM=sam.c
+
+sam:
+ @echo ${SAM:s%.c=acme}
+ @echo ${SAM:s%.c=a%.d}
+ @echo ${SAM:s.c=a%.d}
+ @echo ${SAM:sam.c=a%.c}
+ @echo ${SAM:%=a%.c}
+ @echo ${SAM:%.c=a%.c}
+ @echo ${SAM:sam%=a%.c}
+
+BLA=
+
+bla:
+ @echo $(BLA:%=foo/%x)
diff --git a/unit-tests/varmod-edge.exp b/unit-tests/varmod-edge.exp
new file mode 100644
index 000000000000..b3b2e3a92f95
--- /dev/null
+++ b/unit-tests/varmod-edge.exp
@@ -0,0 +1,17 @@
+make: Unclosed variable specification (expecting '}') for "" (value "*)") modifier U
+make: Unclosed substitution for INP.eq-esc (= missing)
+ok M-paren
+ok M-mixed
+ok M-unescape
+ok M-nest-mix
+ok M-nest-brk
+ok M-pat-err
+ok M-bsbs
+ok M-bs1-par
+ok M-bs2-par
+ok M-128
+ok eq-ext
+ok eq-q
+ok eq-bs
+ok eq-esc
+exit status 0
diff --git a/unit-tests/varmod-edge.mk b/unit-tests/varmod-edge.mk
new file mode 100644
index 000000000000..561eb6116891
--- /dev/null
+++ b/unit-tests/varmod-edge.mk
@@ -0,0 +1,162 @@
+# $NetBSD: varmod-edge.mk,v 1.7 2020/04/27 14:07:22 christos Exp $
+#
+# Tests for edge cases in variable modifiers.
+#
+# These tests demonstrate the current implementation in small examples.
+# They may contain surprising behavior.
+#
+# Each test consists of:
+# - INP, the input to the test
+# - MOD, the expression for testing the modifier
+# - EXP, the expected output
+
+TESTS+= M-paren
+INP.M-paren= (parentheses) {braces} (opening closing) ()
+MOD.M-paren= ${INP.M-paren:M(*)}
+EXP.M-paren= (parentheses) ()
+
+# The first closing brace matches the opening parenthesis.
+# The second closing brace actually ends the variable expression.
+#
+# XXX: This is unexpected but rarely occurs in practice.
+TESTS+= M-mixed
+INP.M-mixed= (paren-brace} (
+MOD.M-mixed= ${INP.M-mixed:M(*}}
+EXP.M-mixed= (paren-brace}
+
+# After the :M modifier has parsed the pattern, only the closing brace
+# and the colon are unescaped. The other characters are left as-is.
+# To actually see this effect, the backslashes in the :M modifier need
+# to be doubled since single backslashes would simply be unescaped by
+# Str_Match.
+#
+# XXX: This is unexpected. The opening brace should also be unescaped.
+TESTS+= M-unescape
+INP.M-unescape= ({}): \(\{\}\)\: \(\{}\):
+MOD.M-unescape= ${INP.M-unescape:M\\(\\{\\}\\)\\:}
+EXP.M-unescape= \(\{}\):
+
+# When the :M and :N modifiers are parsed, the pattern finishes as soon
+# as open_parens + open_braces == closing_parens + closing_braces. This
+# means that ( and } form a matching pair.
+#
+# Nested variable expressions are not parsed as such. Instead, only the
+# parentheses and braces are counted. This leads to a parse error since
+# the nested expression is not "${:U*)}" but only "${:U*)", which is
+# missing the closing brace. The expression is evaluated anyway.
+# The final brace in the output comes from the end of M.nest-mix.
+#
+# XXX: This is unexpected but rarely occurs in practice.
+TESTS+= M-nest-mix
+INP.M-nest-mix= (parentheses)
+MOD.M-nest-mix= ${INP.M-nest-mix:M${:U*)}}
+EXP.M-nest-mix= (parentheses)}
+# make: Unclosed variable specification (expecting '}') for "" (value "*)") modifier U
+
+# In contrast to parentheses and braces, the brackets are not counted
+# when the :M modifier is parsed since Makefile variables only take the
+# ${VAR} or $(VAR) forms, but not $[VAR].
+#
+# The final ] in the pattern is needed to close the character class.
+TESTS+= M-nest-brk
+INP.M-nest-brk= [ [[ [[[
+MOD.M-nest-brk= ${INP.M-nest-brk:M${:U[[[[[]}}
+EXP.M-nest-brk= [
+
+# The pattern in the nested variable has an unclosed character class.
+# No error is reported though, and the pattern is closed implicitly.
+#
+# XXX: It is unexpected that no error is reported.
+# See str.c, function Str_Match.
+#
+# Before 2019-12-02, this test case triggered an out-of-bounds read
+# in Str_Match.
+TESTS+= M-pat-err
+INP.M-pat-err= [ [[ [[[
+MOD.M-pat-err= ${INP.M-pat-err:M${:U[[}}
+EXP.M-pat-err= [
+
+# The first backslash does not escape the second backslash.
+# Therefore, the second backslash escapes the parenthesis.
+# This means that the pattern ends there.
+# The final } in the output comes from the end of MOD.M-bsbs.
+#
+# If the first backslash were to escape the second backslash, the first
+# closing brace would match the opening parenthesis (see M-mixed), and
+# the second closing brace would be needed to close the variable.
+# After that, the remaining backslash would escape the parenthesis in
+# the pattern, therefore (} would match.
+TESTS+= M-bsbs
+INP.M-bsbs= (} \( \(}
+MOD.M-bsbs= ${INP.M-bsbs:M\\(}}
+EXP.M-bsbs= \(}
+#EXP.M-bsbs= (} # If the first backslash were to escape ...
+
+# The backslash in \( does not escape the parenthesis, therefore it
+# counts for the nesting level and matches with the first closing brace.
+# The second closing brace closes the variable, and the third is copied
+# literally.
+#
+# The second :M in the pattern is nested between ( and }, therefore it
+# does not start a new modifier.
+TESTS+= M-bs1-par
+INP.M-bs1-par= ( (:M (:M} \( \(:M \(:M}
+MOD.M-bs1-par= ${INP.M-bs1-par:M\(:M*}}}
+EXP.M-bs1-par= (:M}}
+
+# The double backslash is passed verbatim to the pattern matcher.
+# The Str_Match pattern is \\(:M*}, and there the backslash is unescaped.
+# Again, the ( takes place in the nesting level, and there is no way to
+# prevent this, no matter how many backslashes are used.
+TESTS+= M-bs2-par
+INP.M-bs2-par= ( (:M (:M} \( \(:M \(:M}
+MOD.M-bs2-par= ${INP.M-bs2-par:M\\(:M*}}}
+EXP.M-bs2-par= \(:M}}
+
+# Str_Match uses a recursive algorithm for matching the * patterns.
+# Make sure that it survives patterns with 128 asterisks.
+# That should be enough for all practical purposes.
+# To produce a stack overflow, just add more :Qs below.
+TESTS+= M-128
+INP.M-128= ${:U\\:Q:Q:Q:Q:Q:Q:Q:S,\\,x,g}
+PAT.M-128= ${:U\\:Q:Q:Q:Q:Q:Q:Q:S,\\,*,g}
+MOD.M-128= ${INP.M-128:M${PAT.M-128}}
+EXP.M-128= ${INP.M-128}
+
+# This is the normal SysV substitution. Nothing surprising here.
+TESTS+= eq-ext
+INP.eq-ext= file.c file.cc
+MOD.eq-ext= ${INP.eq-ext:%.c=%.o}
+EXP.eq-ext= file.o file.cc
+
+# The SysV := modifier is greedy and consumes all the modifier text
+# up until the closing brace or parenthesis. The :Q may look like a
+# modifier, but it really isn't, that's why it appears in the output.
+TESTS+= eq-q
+INP.eq-q= file.c file.cc
+MOD.eq-q= ${INP.eq-q:%.c=%.o:Q}
+EXP.eq-q= file.o:Q file.cc
+
+# The = in the := modifier can be escaped.
+TESTS+= eq-bs
+INP.eq-bs= file.c file.c=%.o
+MOD.eq-bs= ${INP.eq-bs:%.c\=%.o=%.ext}
+EXP.eq-bs= file.c file.ext
+
+# Having only an escaped = results in a parse error.
+# The call to "pattern.lhs = VarGetPattern" fails.
+TESTS+= eq-esc
+INP.eq-esc= file.c file...
+MOD.eq-esc= ${INP.eq-esc:a\=b}
+EXP.eq-esc= # empty
+# make: Unclosed substitution for INP.eq-esc (= missing)
+
+all:
+.for test in ${TESTS}
+. if ${MOD.${test}} == ${EXP.${test}}
+ @printf 'ok %s\n' ${test:Q}''
+. else
+ @printf 'error in %s: expected %s, got %s\n' \
+ ${test:Q}'' ${EXP.${test}:Q}'' ${MOD.${test}:Q}''
+. endif
+.endfor