Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions test/lib/pgxntool-loops.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Every loop in pgxntool's *.mk files, with the test proving that a failing
# iteration fails make. The steps for adding or updating an entry are in
# test/standard/make-loops.bats's failure message.
#
# test/standard/make-loops.bats fails on any loop missing from this list, any
# entry with no matching loop, any loop whose line no longer matches its
# entry's fingerprint, and any coverage test that doesn't exist. It prints the
# fingerprint of each new or changed loop.
#
# Format: five tab-separated fields per line. Lines starting with # and blank
# lines are ignored; order doesn't matter.
# <file> path under pgxntool/
# <context> where the loop is, never a line number:
# rule:<target> a recipe or rule line; <target> is the
# unexpanded text before the rule's ':'
# var:<name> a variable assignment
# define:<name> a define body
# toplevel any other line, e.g. $(foreach ...$(eval ...))
# <kind> for, while or until (shell), or foreach (make)
# <fingerprint> cksum's CRC of the logical line containing the loop, with
# continuations joined and each run of blanks collapsed to
# one space; loops on one line share it
# <coverage> test:<path under test/>:<exact @test name>
# make-time:<why> only for a $(foreach) that runs inside make
# and emits no shell commands
# not-a-loop:<why> only for a shell for/while/until match that
# reading the line shows isn't a loop; <why>
# says what the text is
# untested:<why> grandfathered entries only, never a new loop
# A context with two loops of the same kind gets two lines.

base.mk rule:$(_PGXNTOOL_INSTALL_SCHEDULE) for 2140132506 untested:grandfathered; an iteration can only fail appending to the schedule file that the `>` just before the loop created, so there is no practical way to fail a non-last one
base.mk rule:check-duplicate-docs foreach 3110729658 make-time:expands to $(warning) calls; the recipe itself runs only `:`
base.mk rule:pgtle for 3542486984 test:standard/multi-extension.bats:make pgtle fails when pgtle.sh fails for a non-last extension
base.mk rule:print-pgtle foreach 2102040647 test:standard/multi-extension.bats:make print-pgtle fails when cat fails for a non-last extension
base.mk rule:results for 2360069330 test:standard/make-test.bats:make results fails when a copy other than the last one fails (pgxntool#140)
base.mk rule:results-build for 2438470538 test:standard/make-test.bats:results-build fails when a copy fails (pgxntool#140)
base.mk toplevel foreach 4171952040 make-time:$(eval)s an ASCIIDOC_template rule for each of ASCIIDOC_EXTS
base.mk var:ASCIIDOC_FILES foreach 3923359060 make-time:builds a file list (outer $(foreach))
base.mk var:ASCIIDOC_FILES foreach 3923359060 make-time:builds a file list (inner $(foreach))
base.mk var:ASCIIDOC_HTML foreach 2225361476 make-time:builds a file list
base.mk var:DOCS foreach 510455491 make-time:builds a file list
base.mk var:_PGXNTOOL_DUPLICATE_DOCS foreach 3031171085 make-time:builds a name list
base.mk var:_pgxntool_docs_named foreach 2395367995 make-time:builds a file list
202 changes: 202 additions & 0 deletions test/lib/scan-make-loops.awk
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
# Lists the loops in one makefile, one "<file>\t<context>\t<kind>\t<text>" line
# each, <text> being the whole logical line containing the loop with each run
# of blanks collapsed to one space; make-loops.bats fingerprints it.
# test/standard/make-loops.bats says what counts as a loop and why; the header
# of test/lib/pgxntool-loops.txt describes the context keys.
#
# Usage: awk -v file=<name to print> -f scan-make-loops.awk <makefile>
#
# Every logical line is searched for both kinds of loop; classifying the line
# (recipe, define body, assignment, rule header, directive, other) only picks
# its context key and which text is searched:
# - recipe and define body lines: all of it, minus shell comment lines
# - assignments: the value, whose start counts as command position
# - rule headers: the prerequisites, plus any `; recipe` (taken from the raw
# line, since make doesn't strip comments from it)
# - everything else: the line minus its make comment
# A for/while/until at command position counts only when a `do` follows it in
# the searched text, except in a define body, where every one counts.

BEGIN {
# Shell command position: start of the text, after a separator or opening
# character (quotes included, since `sh -c "..."` bodies are real shell),
# after a keyword that starts a command list, or at the start of $(shell).
CMDPOS = "(^|[;&|(){!`\"']|(^|[^A-Za-z0-9_])(do|then|else|elif)[ \t]|\\$+[({]shell[ \t])[ \t]*"
# `for NAME` must be followed by in/;/do, so prose ("for a reason") and
# awk's `for(i in A)` don't match.
LOOP = "(for[ \t]+[A-Za-z_][A-Za-z0-9_]*([ \t]+in([ \t;]|$)|[ \t]*;|[ \t]+do([ \t;]|$))|for[ \t]*\\(\\(|while([ \t({!]|$)|until([ \t({!]|$))"
SHELL_LOOP = CMDPOS LOOP
# The `do` that a real loop's keyword must be followed by outside a define
# body: after one of ; & ) }, or right after `for NAME`.
DO_AFTER = "[;&)}][ \t]*do([ \t]|$)"
FOR_NAME_DO = "^for[ \t]+[A-Za-z_][A-Za-z0-9_]*[ \t]+do([ \t;]|$)"
}

# Physical lines are buffered until one doesn't end in an odd number of
# backslashes. phys[] keeps them apart because a shell comment ends at its own
# physical line even when that line is continued.
{
if (match($0, /\\+$/) && RLENGTH % 2 == 1) {
phys[++np] = substr($0, 1, length($0) - 1)
next
}
phys[++np] = $0
logical()
np = 0
}
END { if (np) logical() }

function logical( i, raw, r, t, p, s, name, key) {
raw = phys[1]
for (i = 2; i <= np; i++) raw = raw " " phys[i]
logical_text = norm(raw)

# Like make, only lines that don't start with a tab can nest or end a define.
if (defdepth) {
if (raw !~ /^\t/) {
if (np == 1 && raw ~ /^[ \t]*endef([ \t#]|$)/) { defdepth--; return }
if (raw ~ /^[ \t]*((override|export|private)[ \t]+)*define([ \t]|$)/) defdepth++
}
recipe("define:" defname)
return
}

# A tab-led line is a recipe line only while a rule's recipe can follow;
# elsewhere make parses it as an ordinary line (e.g. an indented assignment).
if (inrule && raw ~ /^\t/) { recipe(ctx); return }

r = raw
sub(/^[ \t]+/, "", r)
p = find0(r, "#")
t = p ? substr(r, 1, p - 1) : r
if (t ~ /^[ \t]*$/) return

if (t ~ /^((override|export|private)[ \t]+)*define([ \t]|$)/) {
name = t
sub(/^((override|export|private)[ \t]+)*define[ \t]*/, "", name)
sub(/[ \t]*(:::=|::=|:=|[+?!]?=)[ \t]*$/, "", name)
defname = norm(name)
defdepth = 1
inrule = 0
return
}

# Conditionals don't end a rule's recipe; the other directives do.
if (t ~ /^(ifeq|ifneq|ifdef|ifndef|else|endif)([ \t(]|$)/) {
search(t, "toplevel")
return
}
if (t ~ /^(-?include|sinclude|-?load|vpath|unexport|undefine)([ \t]|$)/) {
search(t, "toplevel")
inrule = 0
return
}

p = find0(t, ":=")
if (p && (substr(t, p, 1) == "=" || substr(t, p) ~ /^(:=|::=|:::=)/)) {
name = substr(t, 1, p - 1)
if (substr(t, p, 1) == "=") sub(/[+?!]$/, "", name)
sub(/^((override|export|private)[ \t]+)+/, "", name)
key = "var:" norm(name)
search_foreach(t, key)
match(substr(t, p), /^:*=/)
search_shell(substr(t, p + RLENGTH), key)
inrule = 0
return
}

if (p) {
ctx = "rule:" norm(substr(t, 1, p - 1))
inrule = 1
s = find0(substr(r, p + 1), ";#")
if (s && substr(r, p + s, 1) == ";") {
search(substr(r, 1, p + s - 1), ctx)
search(substr(r, p + s + 1), ctx)
} else {
search(t, ctx)
}
return
}

search(t, "toplevel")
inrule = 0
}

# A recipe line, or a define body line (which make may later run as one).
# A physical line whose first non-blank character (after the @+- prefixes, on
# a recipe's first line) is # is a shell comment: skipped, but its newline
# still ends the command before it, hence the ";".
function recipe(key, i, p, sh) {
sh = ""
for (i = 1; i <= np; i++) {
p = phys[i]
if (i == 1) sub(/^[ \t@+-]*/, "", p)
else sub(/^[ \t]+/, "", p)
sh = sh (p ~ /^#/ ? " ;" : " " p)
}
search(sh, key)
}

function search(s, key) {
search_foreach(s, key)
search_shell(s, key)
}

function search_foreach(s, key) {
while (match(s, /\$+[({]foreach[ \t]/)) {
emit(key, "foreach")
s = substr(s, RSTART + RLENGTH)
}
}

function search_shell(s, key, at, m, kind) {
while (match(s, SHELL_LOOP)) {
at = RSTART
m = substr(s, RSTART, RLENGTH)
match(m, /for|while|until/)
at += RSTART - 1
kind = substr(m, RSTART, RLENGTH)
m = substr(m, RSTART)
if (key ~ /^define:/ || substr(s, at + RLENGTH) ~ DO_AFTER || m ~ FOR_NAME_DO)
emit(key, kind)
# Resume just past the keyword's first character; the "_" keeps the
# remainder's start from counting as command position.
s = "_" substr(s, at + 1)
}
}

function emit(key, kind) {
print file "\t" key "\t" kind "\t" logical_text
}

# Position of the first character of s that is in set and outside any
# $(...)/${...} reference, or 0. $$ and backslash-escaped characters never
# match.
function find0(s, set, i, n, c, d) {
n = length(s)
d = 0
for (i = 1; i <= n; i++) {
c = substr(s, i, 1)
if (c == "\\") { i++; continue }
if (d) {
if (c == "(" || c == "{") d++
else if (c == ")" || c == "}") d--
continue
}
if (c == "$") {
c = substr(s, i + 1, 1)
if (c == "(" || c == "{") d++
i++
continue
}
if (index(set, c)) return i
}
return 0
}

function norm(s) {
gsub(/[ \t]+/, " ", s)
sub(/^ /, "", s)
sub(/ $/, "", s)
return s
}
Loading
Loading