#!/usr/bin/env python
#
# This file is Copyright (c) 2010 by the GPSD project
# BSD terms apply: see the file COPYING in the distribution root for details.
"""
usage: regress-builder [-r limit] [-s seed]

Test-compile gpsd with each of every possible combination of device-driver 
options, excluding stub drivers we don't support yet.  This does a good
job of catching bugs after driver API changes.

The -r option sets Monte Carlo mode (random-walk through configuration space)
and limits the number of tests.  The -s, if given, sets a seed; the default is
to seed from the system time.

If no options are given, the script performs a sequential test of all
possibilities.  This requires time on the order of 2 ** n where n is
the count of options.
"""
import os, sys, random, getopt, time

driver_names = (
    "aivdm",
    "ashtech",
    "earthmate",
    "evermore",
    "fv18",
    "garmin",
    "garmintxt",
    "gpsclock",
    "itrax",
    "mtk3301",
    "navcom",
    "nmea",
    "ntrip",
    "oncore",
    "oceanserver",
    "rtcm104v2",
    "rtcm104v3",
    "sirf",
    "superstar2",
    "tnt",
    "tripmate",
    "tsip",
    "ubx",
)


cyclesize = 2 ** len(driver_names)

def Sequential():
    "Generate sequential test vectors for exhautive search."
    for i in xrange(cyclesize):
        yield i
    return

def MonteCarlo(seed):
    "Generate a random shuffle of test vectors for Monte Carlo testing."
    # All the magic here is in the choice of modulus.  Any odd number will
    # be relatively prime to any power of two and thus be sufficient to step
    # around the cycle hitting each number exactly once. We'd like adjacent
    # stops to have a large distance from each other.  Number theory says the
    # best way to do this is to choose a modulus close to cyclesize / phi,
    # where phi is the golden ratio (1 + 5**0.5)/2.
    modulus = int(cyclesize / 1.618033980)
    if modulus % 2 == 0:
        modulus += 1
    for i in xrange(cyclesize):
        yield (seed + i * modulus) % cyclesize
    return

class TestFactory:
    "Manage compilation tests."
    Preamble = '''
env	X_LIBS="" \\
	CPPFLAGS="-I/usr/local/include " \\
	LDFLAGS=" -L/usr/local/lib -g" \\
	CFLAGS="-g -O2 -W -Wall" \\
./configure --prefix=/home/gpsd --disable-shared \\
	--without-x \\
	--disable-itrax \\
'''
    def n2v(self, n):
        "Number to test-vector"
        v = [0] * len(driver_names)
        i = 0
        while n > 0:
            v[i] = n % 2
            i += 1
            n = n >> 1
        return v
    def test_header(self, vector):
        hdr = ""
        for (i, name) in enumerate(driver_names):
            hdr += "--" + ("disable", "enable")[vector[i]] + "-" + name + " "
        return hdr[:-1]
    def make(self, vector):
        if os.system("make 2>&1 > /dev/null"):
            print "FAILED:   ", self.test_header(vector) 
        else:
            print "SUCCEEDED:", self.test_header(vector)
    def configure_build(self, vector):
        test = TestFactory.Preamble
        for (i, name) in enumerate(driver_names):
            test += "        --" + ("disable", "enable")[vector[i]] + "-" + name + " \\\n"
        test += "\n"
        if os.system("("+ test + ") >/dev/null"):
            print "configure FAILED:", self.test_header(vector)
        else:
            self.make(vector)
    def filter(self, vector):
        "Tell us whether this combination needs to be tested."
        # No drivers at all won't even configure
        if vector == [0] *  len(driver_names):
            return False
        compiled_in = map(lambda e: e[1], filter(lambda e: e[0], zip(vector, driver_names)))
        def on(name):
            return name in compiled_in
        def off(name):
            return name not in compiled_in
        # START OF FILTERS
        #
        # Some types require NMEA support and shouldn't be built without it.
        if off("nmea") and (on("fv18") or on("earthmate") or on("tripmate")):
            return False
        # Earthmate, FV-18 and TripMate code scopes don't overlap.
        if on("fv18") + on("earthmate") + on("tripmate") not in (0, 3):
            return False
        # SiRF has no overlapping code scopes with the DLE-led protocols
        # (TSIP, Garmin, Evermore) but the DLE-led protocols have
        # overlaps in the packet sniffer.
        if on("sirf") != (on("tsip") or on("garmin") or on("evermore")):
            return False
        #
        # END OF FILTERS
        return True
    def run(self, generator, limit=cyclesize):
        expect = 0
        for i in range(cyclesize):
            if self.filter(self.n2v(i)):
                expect += 1
        print "# Expect %d of %d tests." % (expect, cyclesize)
        starttime = time.time()
        included = 0
        excluded = 0
        for n in generator:
            if limit == 0:
                return
            vector = self.n2v(n)
            if not self.filter(vector):
                print "EXCLUDED:", self.test_header(vector)
                excluded += 1
            else:
                self.configure_build(vector)
                included += 1
                limit -= 1
        elapsed = int(time.time() - starttime)
        hours = elapsed/3600
        minutes = (elapsed - (hours * 3600)) / 60
        seconds = elapsed - (hours * 3600) - (minutes * 60)
        print "%d tests, %d excluded, in %dh%dm%ds" % (included, excluded, hours, minutes, seconds)

if __name__ == '__main__':
    try:
        (options, arguments) = getopt.getopt(sys.argv[1:], "n:r:")
    except getopt.GetoptError, msg:
        print "regress-builder: " + str(msg)
        raise SystemExit, 1
    montecarlo = False
    limit = cyclesize
    seed = int(time.time())
    for (switch, val) in options:
        if switch == '-r':
            montecarlo = True
            limit = int(val)
        elif switch == '-s':
            montecarlo = True
            seed = int(val)
    if montecarlo:
        print "Monte Carlo test seeded with %d" % seed
        TestFactory().run(MonteCarlo(seed), limit)
    else:
        print "Sequential compilation test"
        TestFactory().run(Sequential())
