Add a testsuite (./testsuite) and dependency-logging command stuff that doesn't
authorMatt McCutchen <hashproduct@gmail.com>
Wed, 13 Jun 2007 01:31:06 +0000 (21:31 -0400)
committerMatt McCutchen <hashproduct@gmail.com>
Wed, 13 Jun 2007 01:31:06 +0000 (21:31 -0400)
do anything yet, make a bunch of minor improvements, and move mage.mk to the top
level of the Mage tree.

demo/mage.mk
mage.mk [moved from src/mage.mk with 76% similarity]
testsuite [new file with mode: 0755]

index 73c6b44..d428552 120000 (symlink)
@@ -1 +1 @@
-../src/mage.mk
\ No newline at end of file
+../mage.mk
\ No newline at end of file
similarity index 76%
rename from src/mage.mk
rename to mage.mk
index 658474d..d9ad5d7 100644 (file)
+++ b/mage.mk
@@ -1,6 +1,7 @@
 # Mage Build Tool by Matt McCutchen
 # http://www.kepreon.com/~matt/mage/
 
+
 # Remember the original default goal so we can restore it at the end of mage.mk.
 mg-orig-default-goal:=$(.DEFAULT_GOAL)
 
@@ -13,6 +14,7 @@ mg-orig-default-goal:=$(.DEFAULT_GOAL)
 # TEXT UTILITIES
 empty:=
 bs:=\$(empty)
+hash:=\#
 pct:=%
 define nl
 
@@ -22,7 +24,7 @@ endef
 sq='$(subst ','\'',$1)'
 # Make-quote: a$\nb => a$$$(nl)b
 # This is enough to assign the value, *but not to use it as an argument!*
-mqas=$(subst $(nl),$$(nl),$(subst $$,$$$$,$1))
+mqas=$(subst $(hash),$$(hash),$(subst $(nl),$$(nl),$(subst $$,$$$$,$1)))
 # Return nonempty if the strings are equal, empty otherwise.
 # If the strings have only nice characters, you can do $(filter x$1,x$2).
 streq=$(findstring x$1,$(findstring x$2,x$1))
@@ -177,11 +179,9 @@ mg+ = $(filter-out MG-% $(opfx)%,$+)
 # Replaces references to automatic variables with references to their Mage-ized
 # counterparts.  There might be false matches, e.g., $$@ => $$(mg@) ;
 # to prevent that, write $$$(empty)@ instead.  (c.f. autoconf empty quadrigraph)
-define mg-translate-cmd
-$(subst $$?,$$(mg?),$(subst $$<,$$(mg<),$(subst $$^,$$(mg^),$(subst $$+,$$(mg+),$1))))
-endef
+mg-translate-cmd=$(subst $$t,$$@.tmp,$(subst $$?,$$(mg?),$(subst $$<,$$(mg<),$(subst $$^,$$(mg^),$(subst $$+,$$(mg+),$1)))))
 
-# $(call prereq-predict,target,prerequisites)
+# $(call mg-prereq-predict,target,prerequisites)
 # Expands to code that does the following at second-expansion time:
 # 1. Computes the actual prerequisites from the given prerequisite patterns by
 #    translating % to $* and prepending a directory if appropriate.
@@ -190,8 +190,8 @@ endef
 # Factor out set-gdeps because make gets confused if the macro for the
 # prerequisites has a colon.  Make replaces the first % with the stem *before
 # expansion*, so use $(pct) to protect some %s.
-set-gdeps=$(eval $@@gdeps:=$1)
-prereq-predict=$$$$(call set-gdeps,$$$$+ $(if $(findstring /,$1),$$$$(subst $$$$(pct),$$$*,$2),$$$$(addprefix $$$$(dir $$$$@),$$$$(subst $$$$(pct),$$$$*,$2))))
+mg-set-gdeps=$(eval $@@gdeps:=$1)
+mg-prereq-predict=$$$$(call mg-set-gdeps,$$$$+ $(if $(findstring /,$1),$$$$(subst $$$$(pct),$$$*,$2),$$$$(addprefix $$$$(dir $$$$@),$$$$(subst $$$$(pct),$$$$*,$2))))
 
 # Used to make the rule for bar always run so we can act based on $(bar@gdeps)
 # and check for command change.
@@ -201,37 +201,33 @@ MG-FORCE:
 # $(call mg-define-rule,target,prerequisites,cmd)
 # Defines a rule.  cmd is expanded again when it is run, at which time
 # Mage-ized automatic variables are available.
-define mg-define-rule
-$(eval 
-
-# Store the new command.
-$1: cmd=$(call mg-translate-cmd,$3)
-# Provide variable for the temporary file. FIX
-$1: t=$$@.tmp
-
+#
+# I eradicated the target-specific variables because they fail when there are
+# multiple implicit rules with the same target pattern.
+#
 # Rule for the target.  Set $(bar@gdeps) to all prerequisites.  Apply the new
 # prerequisites.  The command script always runs.  Depend on bar.g to get
 # $(bar@gq).
-$1: $(call prereq-predict,$1,$2) $2 MG-FORCE $(mg-genfile-oid)$(aname)$$$$@.g
-       $$(mg-rule-cmd)
-)
+define mg-define-rule
+$(eval $1: $(call mg-prereq-predict,$1,$2) $2 MG-FORCE $(mg-genfile-oid)$(aname)$$$$@.g
+       $$(call mg-rule-cmd,$(call mg-translate-cmd,$3)))
 endef
 
 # TODO Provide a way to define static pattern rules.
 
-# Procedure to generate bar.  Remember, $@ is bar.g.
+# Procedure to generate bar.  Now $@ is bar, so we say $@.g for bar.g.
 # If an override:
 #     1. Complain.  (Should we stop "`bar' is up to date" using @:; ?)
-#     2. Clear out the warnings so we don't replay them.
+#     2. This would be the place to clear bar's metadata if we wanted to.
 # Otherwise, check prereqs, command, and nonexistence to decide whether bar
 # needs to be regenerated.  If so:
-#     1. Set a flag so Mage knows to reread the genfile when make runs target
-#        "bar".
+#     1. Note that the genfile is about to change.
 #     2. Echo the command being run.
 #     On error, skip to 8:
 #        3. Open the new genfile bar.g.tmp for writing.
-#        4. Store the command in bar.g.tmp.
-#        5. Run the command to bar.tmp, storing warnings in bar.g.tmp.
+#        4. Run the command to bar.tmp, storing warnings in bar.g.tmp.
+#        5. Store the command in bar.g.tmp.  Do it here to ensure that bar.g.tmp
+#           is at least as new as bar.tmp.
 #        6. If bar.tmp differs from bar:
 #           a. Clear and touch bar.g (so bar doesn't become an override if we're
 #              killed between steps 6b and 7).
@@ -243,39 +239,81 @@ endef
 #     1. If there were warnings, replay them.  (HMMM To stderr?)
 # HMMM .tmp in displayed command looks ugly
 define mg-rule-cmd
-       $(foreach x,$@ $(mg+),$(eval $x@checked:=1))
        $(if $(filter $(mg-scout-oid)$(aname)$@,$($@@gq)),\
                $(info Mage: warning: Manually created/modified file at $@ overrides rule.)\
-       ,$(call gload,$@)$(if $($@@gq)$(if $(wildcard $@),,x)$(mg-check-cmd),\
-               $(eval $@@gloaded:=)$(info $(cmd))\
+       ,$(call gload,$@)$(if $($@@gq)$(if $(wildcard $@),,TARGET-DNE)$(mg-check-cmd),\
+               $(eval $@@gloaded:=)$(info $1)\
                @trap 'rm -f $@.tmp $@.g.tmp' EXIT &&\
-               exec 3>$@.g.tmp && $(mg-assign-cmd) >&3 &&\
+               exec 3>$@.g.tmp &&\
                set -o pipefail && { $(mg-run-cmd) | tee /dev/fd/4 | $(mg-wrap-warnings) >&3; } 4>&1 &&\
-               $(mg-maybe-move-target) && mv -f $@.g.tmp $@.g\
+               $(mg-assign-cmd) >&3 &&\
+               $(mg-maybe-move-target) &&\
+               mv -f $@.g.tmp $@.g\
        ,$(if $($@@warnings),\
                $(info $($@@cmd) # Mage warning replay$(nl)$($@@warnings))\
        )))
 endef
 
 # If the command changed, we must regenerate.
-mg-check-cmd=$(if $(call streq,$(cmd),$($@@cmd)),,x)
+mg-check-cmd=$(if $(call streq,$1,$($@@cmd)),,COMMAND-CHANGED)
 
 # Pieces of mg-generate that I factored out to make mg-generate more readable.
-mg-assign-cmd=echo $(call sq,$(call fmt-make-assignment,$@@cmd,$(cmd)))
-mg-run-cmd={ ($(cmd)) 2>&1 && { [ -r $@.tmp ] || { echo 'Mage: error: Command for $@ succeeded without creating it!'; false; }; }; }
+mg-assign-cmd=echo $(call sq,$(call fmt-make-assignment,$@@cmd,$1))
+mg-run-cmd={ ($1) 2>&1 && { [ -r $@.tmp ] || { echo 'Mage: error: Command for $@ succeeded without creating it!'; false; }; }; }
 mg-wrap-warnings=sed -re '1s/^/$@@warnings:=$$(empty)/; 1!s/^/$@@warnings+=$$(nl)/'
-mg-maybe-move-target={ cmp -s $@ $@.tmp || echo >$@.g && mv -f $@.tmp $@; }
+# Drat bash's lack of precedence between || and &&.  Extra braces necessary.
+mg-maybe-move-target={ cmp -s $@ $@.tmp || { echo >$@.g && mv -f $@.tmp $@; }; }
 
 # Just add additional prerequisites.  This cannot add prerequisite patterns to
 # an implicit rule, but it can add specific prerequisites to an individual use
 # of an implicit rule.  Currently, Mage picks up the target's prerequisites from
 # make, so this just attaches the given prerequisites to the target, but the
 # implementation might change in the future.
-define mg-define-prereq
-$(eval $1: $2)
+mg-define-prereq=$(eval $1: $2)
+
+
+# DEPENDENCY-LOGGING COMMANDS
+
+mg-dlc-static-run-oid:=$(mg-genfile-oid)
+
+# $(call mg-define-rule-dlc,target,static-prerequisites,cmd,dep-converter)
+# Analogue of mg-define-rule for a dependency-logging command.
+# I haven't decided on the format for the dep-converter yet.
+define mg-define-rule-dlc
+$(eval 
+
+# FINISH
+
+# Rule for the target.  Set $(bar@gdeps) to all prerequisites.  Apply the new
+# prerequisites.  The command script always runs.  Finally, do the static run
+# and then the first dynamic check.
+$1: $(call mg-prereq-predict,$1,$2) $2 MG-FORCE $(mg-dlc-static-run-oid)$(aname)$$$$@.g  $$$$(call dlc-next-dchk,$$$$(target))
+# TODO: Move the finished file into place or something
+#      $$(call mg-rule-cmd,$3)
+)
 endef
 
-# END
+#mg-dlc-next-dchk=$(call mg-dlc-next-dchk-1,$1,$(newoid))
+#mg-dlc-next-dchk-1=$(eval $2@opr=$$(call mg-dlc-dchk-pr,$1,$2))$2
+#
+## $(call mg-dlc-dchk-pr,target,oid)
+#mg-dlc-dchk-pr=$(call mg-dlc-dchk-pr-1,$1,$2,$(call next-unchecked-prereq,$($1@deps)))
+## $(call mg-next-unchecked-prereq,foo.c bar.h baz.h)
+#mg-next-unchecked-prereq=$(firstword $(foreach p,$1,$(if $($p@checked),,$p)))
+#define mg-dlc-dchk-pr-1
+#$(if $3,$(call mg-dlc-drun,$1,$2,$3) $(call mg-dlc-next-dchk,$1),)
+#endef
+#
+## $(call mg-dlc-drun,target,oid,prereq). FINISH
+#define mg-dlc-drun
+#$(eval 
+#$2$(aname)$3@opr:=$3
+#$2$(aname)$3@ocmd:=
+#)$2$(aname)$3
+#endef
+#
+#
+## END
 
 # We don't want anything we defined to become the default goal.
 .DEFAULT_GOAL := $(mg-orig-default-goal)
diff --git a/testsuite b/testsuite
new file mode 100755 (executable)
index 0000000..3329a66
--- /dev/null
+++ b/testsuite
@@ -0,0 +1,238 @@
+#!/bin/bash
+# Test suite for Mage.
+
+echo "Test suite for Mage"
+
+cd "$(dirname "$0")"
+Z=test-zone
+rm -rf $Z
+mkdir $Z
+cd $Z
+
+#exec 3>&2
+#exec 1>test.log 2>&1
+exec </dev/null
+set -e
+set -o errtrace
+section=Initialization
+trap 'echo; echo "TEST SUITE FAILED in section $section!" >&2' ERR
+#set -x
+
+ln -s ../mage.mk mage.mk
+
+function fail {
+       false
+}
+
+function start_section {
+       section="$1"
+       echo
+       echo "SECTION: $1"
+}
+
+function do_mage {
+       echo "Running: make $*"
+       make "$@" 2>&1 | tee mage.log
+}
+
+function assert_contents {
+       if diff -u - "$1"; then
+               echo "File '$1' looks good."
+       else
+               echo "File '$1' has the wrong contents!"
+               fail
+       fi
+}
+
+# Options can be passed to grep: assert_saw -i override
+function assert_saw {
+       if grep -q "$@" mage.log; then
+               echo "Good, saw '${@:$#}' in build log."
+       else
+               echo "Expected '${@:$#}' in build log but didn't see it!  Log:"
+               cat mage.log
+               fail
+       fi
+}
+function assert_not_saw {
+       if ! grep -q "$@" mage.log; then
+               echo "Good, saw '${@:$#}' in build log."
+       else
+               echo "Did not expect '${@:$#}' in build log but saw it!  Log:"
+               cat mage.log
+               fail
+       fi
+}
+function assert_uptodate {
+       assert_saw "make: \`$1' is up to date."
+}
+function assert_generated {
+       assert_saw "$1.tmp"
+}
+
+function remember_mtime {
+       while [ $# != 0 ]; do
+               eval "orig_mtime_${1//[^A-Za-z]/}=$(stat --format=%Y $1)"
+               shift
+       done
+}
+function assert_touched {
+       mtvar=orig_mtime_${1//[^A-Za-z]/}
+       t1="${!mtvar}"
+       t2="$(stat --format=%Y $1)"
+       echo "Times for '$1': $t1, $t2"
+       if [ "$t1" != "$t2" ]; then
+               echo "Good, '$1' was touched."
+       else
+               echo "Expected '$1' to be touched but it wasn't!"
+               fail
+       fi
+}
+function assert_not_touched {
+       mtvar=orig_mtime_${1//[^A-Za-z]/}
+       t1="${!mtvar}"
+       t2="$(stat --format=%Y $1)"
+       echo "Times for '$1': $t1, $t2"
+       if [ "$t1" == "$t2" ]; then
+               echo "Good, '$1' was not touched."
+       else
+               echo "Expected '$1' to be not touched but it was!"
+               fail
+       fi
+}
+
+# Simple makefile for stripping two kinds of comments.
+# Tests an implicit rule and two competing explicit rules.
+# Watch those dollar signs!
+cat >Makefile <<'EOF'
+include mage.mk
+.SECONDARY:
+include hc-rule.mk
+$(call mg-define-rule,%,%.ssc,sleep 1 && grep 'warn' $$< && sed -e 's_//.*$$$$__' $$< >$$t)
+$(call mg-define-rule,index,index.in,sort $$< >$$t)
+EOF
+cat >hc-rule.mk <<'EOF'
+$(call mg-define-rule,%,%.hc,sed -e 's_#.*$$$$__' $$< >$$t)
+EOF
+
+# Input files.
+cat >foo.hc <<'EOF'
+This is the foo file.
+# You don't get to see this.
+But you do get to see this!
+// Needles: you can lean but you can't hide!
+EOF
+cat >bar.ssc <<'EOF'
+the bar file has a different personality
+// hey there
+# I slip through
+warn: tell me about it!
+EOF
+cat >index.in <<'EOF'
+foo
+bar
+EOF
+
+# Run and make sure the files were compiled correctly.
+
+start_section "Initial full build"
+do_mage foo bar index
+
+assert_contents foo <<'EOF'
+This is the foo file.
+
+But you do get to see this!
+// Needles: you can lean but you can't hide!
+EOF
+assert_contents bar <<'EOF'
+the bar file has a different personality
+
+# I slip through
+warn: tell me about it!
+EOF
+assert_contents index <<'EOF'
+bar
+foo
+EOF
+assert_generated foo
+assert_generated bar
+assert_generated index
+assert_saw "sed -e 's_#.*\$__' foo.hc >foo.tmp"
+assert_saw "sleep 1 && grep 'warn' bar.ssc && sed -e 's_//.*\$__' bar.ssc >bar.tmp"
+assert_saw 'warn: tell me about it!'
+assert_saw sort index.in >index.tmp
+
+# Run it again.  Make sure the warning is replayed and bar isn't overridden due
+# to bar.g accidentally being too old.
+
+start_section "Replay bar warning"
+do_mage foo bar index
+
+assert_uptodate foo
+assert_uptodate bar
+assert_uptodate index
+assert_saw -i '#.*warning.*replay' # Indication of warning replay
+assert_saw 'warn: tell me about it!' # Actual warning
+assert_not_saw -i overrid
+
+# Now override bar and make sure it stays that way and the warning isn't replayed.
+start_section "Override bar"
+sleep 1 # No racy cleanliness
+echo NEWCONTENT >bar
+do_mage bar
+
+assert_saw -i overrid
+assert_contents bar <<<NEWCONTENT
+
+# Change foo.hc and make sure foo is updated properly.
+
+start_section "Change foo.hc"
+sleep 1 # No racy cleanliness
+echo 'look: # Last-minute addition.' >>foo.hc
+do_mage index foo
+
+assert_uptodate index
+assert_generated foo
+assert_contents foo <<'EOF'
+This is the foo file.
+
+But you do get to see this!
+// Needles: you can lean but you can't hide!
+look: 
+EOF
+
+# Change the rule for # comments to strip spaces before a #.
+# Make sure foo is updated properly.
+
+start_section "Command change for % <- %.hc"
+cat >hc-rule.mk <<'EOF'
+$(call mg-define-rule,%,%.hc,sed -e 's_ *#.*$$$$__' $$< >$$t)
+EOF
+do_mage foo
+
+assert_generated foo
+assert_contents foo <<'EOF'
+This is the foo file.
+
+But you do get to see this!
+// Needles: you can lean but you can't hide!
+look:
+EOF
+
+# Remove a space before a # from foo.hc.  This is an inconsequential change.
+# Make sure that foo.g is touched but foo is not.
+
+start_section "Inconsequential change to foo.hc"
+sleep 1 # No racy cleanliness
+remember_mtime foo foo.g
+sed -e '$s/look: /look:/' -i foo.hc
+do_mage foo
+
+assert_generated foo
+assert_touched foo.g
+assert_not_touched foo
+
+cd ..
+rm -rf test-zone
+echo
+echo "TEST SUITE SUCCEEDED"