More tests
authorBenjamin Smedberg <benjamin@smedbergs.us>
Fri, 30 Jan 2009 11:51:22 -0500
changeset 9 39a719db34cc
parent 8 2426e9915a8f
child 10 c647fb430b20
push id7
push userbsmedberg@mozilla.com
push dateFri, 30 Jan 2009 20:28:40 +0000
More tests
pymake/data.py
tests/bad-command-continuation.mk
tests/eof-continuation.mk
tests/escape-chars.mk
tests/escaped-continuation.mk
tests/line-continuations.mk
tests/runtests.py
--- a/pymake/data.py
+++ b/pymake/data.py
@@ -3,16 +3,25 @@
 """
 A representation of makefile data structures.
 """
 
 import logging
 
 log = logging.getLogger('pymake.data')
 
+class DataError(Exception):
+    def __init__(self, message, loc=None):
+        self.message = message
+        self.loc = loc
+
+    def __str__(self):
+        return "%s: %s" % (self.loc and self.loc or "internal error",
+                           self.message)
+
 class Function(object):
     """
     An object that represents a function call. This class is always subclassed
     with the following two methods:
 
     def setup(self)
         validates the number of arguments to a function
         no return value
@@ -31,26 +40,27 @@ class FlavorFunction(Function):
         if len(self._arguments) < 1:
             raise SomeError
         if len(self._arguments) > 1:
             log.warning("Function 'flavor' only takes one argument.")
 
     def resolve(self, variables, setting):
         varname = self._arguments[0].resolve(variables, setting)
 
-        v = variables.get(varname, None)
-        if v is None:
+        
+        flavor, source, value = variables.get(varname, None)
+        if flavor is None:
             return 'undefined'
 
-        if v.flavor == Variable.FLAVOR_RECURSIVE:
+        if flavor == Variables.FLAVOR_RECURSIVE:
             return 'recursive'
-        elif v.flavor == Variable.FLAVOR_SIMPLE:
+        elif flavor == Variables.FLAVOR_SIMPLE:
             return 'simple'
 
-        raise TODODataError('Variable %s flavor is neither simple nor recursive!' % varname)
+        raise DataError('Variable %s flavor is neither simple nor recursive!' % (varname,))
 
 functions = {
     'subst': None,
     'patsubst': None,
     'strip': None,
     'findstring': None,
     'filter': None,
     'filter-out': None,
@@ -80,48 +90,31 @@ functions = {
     'origin': None,
     'flavor': FlavorFunction,
     'shell': None,
     'error': None,
     'warning': None,
     'info': None,
 }
 
-class Variable(object):
+class Expansion(object):
     """
-    An object that represents a string with variable substitutions.
+    A representation of expanded data, such as that for a recursively-expanded variable, a command, etc.
     """
 
-    FLAVOR_RECURSIVE = 0
-    FLAVOR_SIMPLE = 1
-
-    SOURCE_OVERRIDE = 0
-    SOURCE_COMMANDLINE = 1
-    SOURCE_MAKEFILE = 2
-    # I have no intention of supporting values from the environment
-    # SOURCE ENVIRONMENT = 3
-    SOURCE_AUTOMATIC = 4
-    # I have no intention of supporting builtin rules or variables that go with them
-    # SOURCE_IMPLICIT = 5
-
-    def __init__(self, flavor, source):
+    def __init__(self):
+        # Each element is either a string or a function
         self._elements = []
 
-        assert(flavor in (FLAVOR_RECURSIVE, FLAVOR_SIMPLE))
-        self.flavor = flavor
-
-        assert(source in (SOURCE_OVERRIDE, SOURCE_MAKEFILE, SOURCE_AUTOMATIC))
-        self.source = source
+    def append(self, object):
+        if not isinstance(object, (string, Function)):
+            throw DataError("Expansions can contain only strings or functions, got %s" % (type(object),))
 
-    def append(self, object):
         self._elements.append(object)
 
-    def __iter__(self):
-        return iter(self._elements)
-
     def resolve(self, variables, setting):
         """
         Resolve this variable into a value, by interpolating the value
         of other variables.
 
         @param setting (Variable instance) the variable currently
                being set, if any. Setting variables must avoid self-referential
                loops.
@@ -141,54 +134,92 @@ class Target(object):
         self.type = type
         self.rules = []
 
     def addRule(self, rule):
         rules.append(rule)
         # TODO: sanity-check that the rule either has the name of the target,
         # or that the pattern matches. Also maybe that the type matches
 
+class Variables(object):
+    """
+    A mapping from variable names to variables. Variables have flavor, source, and value. The value is an 
+    expansion object.
+    """
+
+    FLAVOR_RECURSIVE = 0
+    FLAVOR_SIMPLE = 1
+
+    SOURCE_OVERRIDE = 0
+    SOURCE_COMMANDLINE = 1
+    SOURCE_MAKEFILE = 2
+    # I have no intention of supporting values from the environment
+    # SOURCE ENVIRONMENT = 3
+    SOURCE_AUTOMATIC = 4
+    # I have no intention of supporting builtin rules or variables that go with them
+    # SOURCE_IMPLICIT = 5
+
+    def __init__(self, parent=None):
+        self._map = {}
+        self.parent = parent
+
+    def get(name):
+        """
+        Get the value of a named variable. Returns a tuple (flavor, source, value)
+
+        If the variable is not present, returns (None, None, None)
+        """
+        v = self._map.get(name, None)
+        if v is not None:
+            return v
+
+        if self.parent is not None:
+            return self.parent.get(name)
+
+        return (None, None, None)
+
+    def set(name, flavor, source, value):
+        if not flavor in (FLAVOR_RECURSIVE, FLAVOR_SIMPLE):
+            raise DataError("Unexpected variable flavor: %s" % (flavor,))
+
+        if not source in (SOURCE_OVERRIDE, SOURCE_MAKEFILE, SOURCE_AUTOMATIC):
+            raise DataError("Unexpected variable source: %s" % (source,))
+
+        if not isinstance(value, Expansion):
+            raise DataError("Unexpected variable value, wasn't an expansion.")
+
+        prevflavor, prevsource, prevvalue = self.get(name)
+        if prevsource is not None and source > prevsource:
+            log.warning("Not setting variable '%s', set by higher-priority source to value '%s'" % (name, prevvalue))
+            return
+
+        self._map[name] = (flavor, source, value)
+
 class Rule(object):
     """
     A rule contains a target and a list of prerequisites. It may also
     contain rule-specific variables.
     """
 
     RULE_ORDINARY = 0
     RULE_DOUBLECOLON = 1
 
-    def __init__(self, target):
+    def __init__(self, target, makefile):
         self.target = target
-        self._dependencies = []
-        self._variables = {}
+        self._prerequisites = []
+        self.variables = Variables(parent=makefile.variables)
 
-    def adddependency(self, d):
-        self._dependencies.append(d)
-
-    def addvariable(self, v, value):
-        self._variables[v] = value
+    def addprerequisite(self, d):
+        self._prerequisites.append(d)
 
 class Makefile(object):
     """
-    A Makefile is a variable dict, a target dict, and a list of rules.
-
-    TODO: should the rules be *all* the rules, or just the pattern rules?
+    A Makefile is a variable dict, a target dict, and a list of the rules and pattern rules.
     """
 
     def __init__(self):
         self._targets = {}
-        self._variables = {}
+        self.variables = Variables()
         self._rules = []
 
-    def setVariable(self, name, value):
-        assert(isinstance(value, Variable))
-
-        if name in self._variables:
-            oldsource = self._variables[name].source
-            if newsource > oldsource:
-                log.warning("Not setting variable '%s', already set to higher priority value." % (name, ))
-                return
-
-        self._variables[name] = value
-
     def addRule(self, rule):
         self._rules.append(rule)
         # TODO: add this to targets!
new file mode 100644
--- /dev/null
+++ b/tests/bad-command-continuation.mk
@@ -0,0 +1,3 @@
+all:
+	echo 'hello'\
+TEST-PASS
new file mode 100644
--- /dev/null
+++ b/tests/eof-continuation.mk
@@ -0,0 +1,6 @@
+#T returncode: 2
+
+all:
+	test "$(TESTVAR)" = "testval"
+
+TESTVAR = testval\
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/tests/escape-chars.mk
@@ -0,0 +1,6 @@
+space = $(NULL) $(NULL)
+hello$(space)world$(space) = hellovalue
+
+all:
+	test "$(hello world )" = "hellovalue"
+	@echo TEST-PASS
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/tests/escaped-continuation.mk
@@ -0,0 +1,6 @@
+#T returncode: 2
+
+all:
+	echo "Hello" \\
+	test "world" = "not!"
+	@echo TEST-PASS
--- a/tests/line-continuations.mk
+++ b/tests/line-continuations.mk
@@ -2,9 +2,9 @@ VAR = val1 	 \
   	  val2  
 
 all:
 	test "$(VAR)" = "val1 val2  "
 	test "hello \
 	  world" = "hello   world"
 	test "hello" = \
 "hello"
-	@echo TEST-PASS
\ No newline at end of file
+	@echo TEST-PASS
--- a/tests/runtests.py
+++ b/tests/runtests.py
@@ -56,18 +56,21 @@ for makefile in makefiles:
             print >>sys.stderr, "Unexpected #T key: %s" % key
             sys.exit(1)
 
     mdata.close()
 
     p = Popen(cline, stdout=PIPE, stderr=STDOUT)
     stdout, d = p.communicate()
     if p.returncode != returncode:
-        print "FAIL"
+        print "FAIL (returncode=%i)" % p.returncode
         print stdout
     elif stdout.find('TEST-FAIL') != -1:
         print "FAIL"
         print stdout
-    elif stdout.find('TEST-PASS') != -1:
-        print "PASS"
+    elif returncode == 0:
+        if stdout.find('TEST-PASS') != -1:
+            print "PASS"
+        else:
+            print "FAIL (no passing output)"
+            print stdout
     else:
-        print "FAIL (no passing output)"
-        print stdout
+        print "EXPECTED-FAIL"