summaryrefslogtreecommitdiff
path: root/test/test_farm.py
blob: f6f646f6bb699e6a2188967646d6e5d694abb057 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
"""Run tests in the farm subdirectory.  Designed for nose."""

import filecmp, fnmatch, glob, os, shutil, sys

try:
    import subprocess
except ImportError:
    subprocess = None


def test_farm(clean_only=False):
    """A test-generating function for nose to find and run."""
    for fname in glob.glob("test/farm/*/*.py"):
        case = FarmTestCase(fname, clean_only)
        yield (case,)


class FarmTestCase(object):
    """A test case from the farm tree.
    
    Tests are short Python script files, often called run.py:
    
        copy("src", "out")
        run('''
            coverage -x white.py
            coverage -a white.py
            ''', rundir="out")
        compare("out", "gold", "*,cover")
        clean("out")

    Verbs (copy, run, compare, clean) are methods in this class.  FarmTestCase
    has options to allow various uses of the test cases (normal execution,
    cleaning-only, or run and leave the results for debugging).
    
    """
    def __init__(self, runpy, clean_only=False, dont_clean=False):
        """Create a test case from a run.py file.
        
        `clean_only` means that only the clean() action is executed.
        `dont_clean` means that the clean() action is not executed.
        
        """
        self.description = runpy
        self.dir, self.runpy = os.path.split(runpy)
        self.clean_only = clean_only
        self.dont_clean = dont_clean

    def cd(self, newdir):
        """Change the current directory, and return the old one."""
        cwd = os.getcwd()
        os.chdir(newdir)
        return cwd

    def __call__(self):
        """Execute the test from the run.py file.
        
        """
        cwd = self.cd(self.dir)

        # Prepare a dictionary of globals for the run.py files to use.
        fns = "copy run compare clean".split()
        if self.clean_only:
            glo = dict([(fn, self.noop) for fn in fns])
            glo['clean'] = self.clean
        else:
            glo = dict([(fn, getattr(self, fn)) for fn in fns])
            if self.dont_clean:
                glo['clean'] = self.noop
        
        try:
            execfile(self.runpy, glo)
        finally:
            self.cd(cwd)

    def fnmatch_list(self, files, filepattern):
        """Filter the list of `files` to only those that match `filepattern`.
        
        Returns a string naming the filtered files.
        
        """
        files = [f for f in files if fnmatch.fnmatch(f, filepattern)]
        return ", ".join(files)

    def setUp(self):
        """Test set up, run by nose before __call__."""
        pass
    
    def tearDown(self):
        """Test tear down, run by nose after __call__."""
        # Make sure no matter what, the test is cleaned up.
        self.clean_only = True
        self()

    # Functions usable inside farm run.py files
    
    def noop(self, *args, **kwargs):
        """A no-op function to stub out run, copy, etc, when only cleaning."""
        pass
    
    def copy(self, src, dst):
        """Copy a directory."""

        if os.path.exists(dst):
            shutil.rmtree(dst)
        shutil.copytree(src, dst)

    def run(self, cmds, rundir="src"):
        """Run a list of commands.
        
        `cmds` is a string, commands separated by newlines.
        `rundir` is the directory in which to run the commands.
        
        """
        cwd = self.cd(rundir)
        try:
            for cmd in cmds.split("\n"):
                if subprocess:
                    proc = subprocess.Popen(cmd, shell=True, 
                              stdin=subprocess.PIPE, stdout=subprocess.PIPE,
                              stderr=subprocess.STDOUT)
                    retcode = proc.wait()
                    output = proc.stdout.read()
                else:
                    _, stdouterr = os.popen4(cmd)
                    output = stdouterr.read()
                    retcode = 0 # Can't tell if the process failed.
                print output,
                if retcode:
                    raise Exception("command exited abnormally")
        finally:
            self.cd(cwd)

    def compare(self, dir1, dir2, filepattern=None, left_extra=False,
        right_extra=False
        ):
        """Compare files matching `filepattern` in `dir1` and `dir2`.
        
        `dir2` is interpreted as a prefix, with Python version numbers appended
        to find the actual directory to compare with. "foo" will compare against
        "foo_v241", "foo_v24", "foo_v2", or "foo", depending on which directory
        is found first.
        
        `left_extra` true means the left directory can have extra files in it
        without triggering an assertion.  `right_extra` means the right
        directory can.
        
        An assertion will be raised if the directories don't match in some way.
        
        """
        # Search for a dir2 with a version suffix.
        version_suff = ''.join(map(str, sys.version_info[:3]))
        while version_suff:
            trydir = dir2 + '_v' + version_suff
            if os.path.exists(trydir):
                dir2 = trydir
                break
            version_suff = version_suff[:-1]

        assert os.path.exists(dir1), "Left directory missing: %s" % dir1
        assert os.path.exists(dir2), "Right directory missing: %s" % dir2
            
        dc = filecmp.dircmp(dir1, dir2)
        diff_files = self.fnmatch_list(dc.diff_files, filepattern)
        left_only = self.fnmatch_list(dc.left_only, filepattern)
        right_only = self.fnmatch_list(dc.right_only, filepattern)
        
        assert not diff_files, "Files differ: %s" % (diff_files)
        if not left_extra:
            assert not left_only, "Files in %s only: %s" % (dir1, left_only)
        if not right_extra:
            assert not right_only, "Files in %s only: %s" % (dir2, right_only)

    def clean(self, cleandir):
        """Clean `cleandir` by removing it and all its children completely."""
        if os.path.exists(cleandir):
            shutil.rmtree(cleandir)

def main():
    op = sys.argv[1]
    if op == 'run':
        # Run the test for real.
        case = FarmTestCase(sys.argv[2])
        case()
    elif op == 'out':
        # Run the test, but don't clean up, so we can examine the output.
        case = FarmTestCase(sys.argv[2], dont_clean=True)
        case()
    elif op == 'clean':
        # Run all the tests, but just clean.
        for test in test_farm(clean_only=True):
            test[0](*test[1:])
    else:
        print "Need an operation: run, out, clean"
    
# So that we can run just one farm run.py at a time.
if __name__ == '__main__':
    main()