1#Copyright ReportLab Europe Ltd. 2000-2017
2#see license.txt for license details
3import reportlab
4reportlab._rl_testing=True
5del reportlab
6__version__='3.3.0'
7__doc__="""Provides support for the test suite.
8
9The test suite as a whole, and individual tests, need to share
10certain support functions.  We have to put these in here so they
11can always be imported, and so that individual tests need to import
12nothing more than "reportlab.whatever..."
13"""
14
15import sys, os, fnmatch, re
16try:
17    from configparser import ConfigParser
18except ImportError:
19    from ConfigParser import ConfigParser
20import unittest
21from reportlab.lib.utils import isCompactDistro, __rl_loader__, rl_isdir, asUnicode
22from reportlab import ascii
23
24# Helper functions.
25def isWritable(D):
26    try:
27        fn = '00DELETE.ME'
28        f = open(fn, 'w')
29        f.write('test of writability - can be deleted')
30        f.close()
31        if os.path.isfile(fn):
32            os.remove(fn)
33            return 1
34    except:
35        return 0
36
37_OUTDIR = None
38RL_HOME = None
39testsFolder = None
40def setOutDir(name):
41    """Is it a writable file system distro being invoked within
42    test directory?  If so, can write test output here.  If not,
43    it had better go in a temp directory.  Only do this once per
44    process"""
45    global _OUTDIR, RL_HOME, testsFolder
46    if _OUTDIR: return _OUTDIR
47    D = [d[9:] for d in sys.argv if d.startswith('--outdir=')]
48    if not D:
49        D = os.environ.get('RL_TEST_OUTDIR','')
50        if D: D=[D]
51    if D:
52        _OUTDIR = D[-1]
53        try:
54            os.makedirs(_OUTDIR)
55        except:
56            pass
57        for d in D:
58            if d in sys.argv:
59                sys.argv.remove(d)
60    else:
61        assert name=='__main__',"setOutDir should only be called in the main script"
62        scriptDir=os.path.dirname(sys.argv[0])
63        if not scriptDir: scriptDir=os.getcwd()
64        _OUTDIR = scriptDir
65
66    if not isWritable(_OUTDIR):
67        _OUTDIR = get_rl_tempdir('reportlab_test')
68
69    import reportlab
70    RL_HOME=reportlab.__path__[0]
71    if not os.path.isabs(RL_HOME): RL_HOME=os.path.normpath(os.path.abspath(RL_HOME))
72    topDir = os.path.dirname(RL_HOME)
73    testsFolder = os.path.join(topDir,'tests')
74    if not os.path.isdir(testsFolder):
75        testsFolder = os.path.join(os.path.dirname(topDir),'tests')
76    if not os.path.isdir(testsFolder):
77        if name=='__main__':
78            scriptDir=os.path.dirname(sys.argv[0])
79            if not scriptDir: scriptDir=os.getcwd()
80            testsFolder = os.path.abspath(scriptDir)
81        else:
82            testsFolder = None
83    if testsFolder:
84        sys.path.insert(0,os.path.dirname(testsFolder))
85    return _OUTDIR
86
87def outputfile(fn):
88    """This works out where to write test output.  If running
89    code in a locked down file system, this will be a
90    temp directory; otherwise, the output of 'test_foo.py' will
91    normally be a file called 'test_foo.pdf', next door.
92    """
93    D = setOutDir(__name__)
94    if fn: D = os.path.join(D,fn)
95    return D
96
97def printLocation(depth=1):
98    if sys._getframe(depth).f_locals.get('__name__')=='__main__':
99        outDir = outputfile('')
100        if outDir!=_OUTDIR:
101            print('Logs and output files written to folder "%s"' % outDir)
102
103def makeSuiteForClasses(*classes):
104    "Return a test suite with tests loaded from provided classes."
105
106    suite = unittest.TestSuite()
107    loader = unittest.TestLoader()
108    for C in classes:
109        suite.addTest(loader.loadTestsFromTestCase(C))
110    return suite
111
112def getCVSEntries(folder, files=1, folders=0):
113    """Returns a list of filenames as listed in the CVS/Entries file.
114
115    'folder' is the folder that should contain the CVS subfolder.
116    If there is no such subfolder an empty list is returned.
117    'files' is a boolean; 1 and 0 means to return files or not.
118    'folders' is a boolean; 1 and 0 means to return folders or not.
119    """
120
121    join = os.path.join
122
123    # If CVS subfolder doesn't exist return empty list.
124    try:
125        f = open(join(folder, 'CVS', 'Entries'))
126    except IOError:
127        return []
128
129    # Return names of files and/or folders in CVS/Entries files.
130    allEntries = []
131    for line in f.readlines():
132        if folders and line[0] == 'D' \
133           or files and line[0] != 'D':
134            entry = line.split('/')[1]
135            if entry:
136                allEntries.append(join(folder, entry))
137
138    return allEntries
139
140
141# Still experimental class extending ConfigParser's behaviour.
142class ExtConfigParser(ConfigParser):
143    "A slightly extended version to return lists of strings."
144
145    pat = re.compile(r'\s*\[.*\]\s*')
146
147    def getstringlist(self, section, option):
148        "Coerce option to a list of strings or return unchanged if that fails."
149
150        value = ConfigParser.get(self, section, option)
151
152        # This seems to allow for newlines inside values
153        # of the config file, but be careful!!
154        val = value.replace('\n', '')
155
156        if self.pat.match(val):
157            return eval(val,{__builtins__:None})
158        else:
159            return value
160
161
162# This class as suggested by /F with an additional hook
163# to be able to filter filenames.
164
165class GlobDirectoryWalker:
166    "A forward iterator that traverses files in a directory tree."
167
168    def __init__(self, directory, pattern='*'):
169        self.index = 0
170        self.pattern = pattern
171        directory.replace('/',os.sep)
172        if os.path.isdir(directory):
173            self.stack = [directory]
174            self.files = []
175        else:
176            if not isCompactDistro() or not __rl_loader__ or not rl_isdir(directory):
177                raise ValueError('"%s" is not a directory' % directory)
178            self.directory = directory[len(__rl_loader__.archive)+len(os.sep):]
179            pfx = self.directory+os.sep
180            n = len(pfx)
181            self.files = list(map(lambda x, n=n: x[n:],list(filter(lambda x,pfx=pfx: x.startswith(pfx),list(__rl_loader__._files.keys())))))
182            self.files.sort()
183            self.stack = []
184
185    def __getitem__(self, index):
186        while 1:
187            try:
188                file = self.files[self.index]
189                self.index = self.index + 1
190            except IndexError:
191                # pop next directory from stack
192                self.directory = self.stack.pop()
193                self.files = os.listdir(self.directory)
194                # now call the hook
195                self.files = self.filterFiles(self.directory, self.files)
196                self.index = 0
197            else:
198                # got a filename
199                fullname = os.path.join(self.directory, file)
200                if os.path.isdir(fullname) and not os.path.islink(fullname):
201                    self.stack.append(fullname)
202                if fnmatch.fnmatch(file, self.pattern):
203                    return fullname
204
205    def filterFiles(self, folder, files):
206        "Filter hook, overwrite in subclasses as needed."
207
208        return files
209
210
211class RestrictedGlobDirectoryWalker(GlobDirectoryWalker):
212    "An restricted directory tree iterator."
213
214    def __init__(self, directory, pattern='*', ignore=None):
215        GlobDirectoryWalker.__init__(self, directory, pattern)
216
217        if ignore == None:
218            ignore = []
219        ip = [].append
220        if isinstance(ignore,(tuple,list)):
221            for p in ignore:
222                ip(p)
223        elif isinstance(ignore,str):
224            ip(ignore)
225        self.ignorePatterns = ([_.replace('/',os.sep) for _ in ip.__self__] if os.sep != '/'
226                                else ip.__self__)
227
228    def filterFiles(self, folder, files):
229        "Filters all items from files matching patterns to ignore."
230
231        fnm = fnmatch.fnmatch
232        indicesToDelete = []
233        for i,f in enumerate(files):
234            for p in self.ignorePatterns:
235                if fnm(f, p) or fnm(os.path.join(folder,f),p):
236                    indicesToDelete.append(i)
237        indicesToDelete.reverse()
238        for i in indicesToDelete:
239            del files[i]
240
241        return files
242
243
244class CVSGlobDirectoryWalker(GlobDirectoryWalker):
245    "An directory tree iterator that checks for CVS data."
246
247    def filterFiles(self, folder, files):
248        """Filters files not listed in CVS subfolder.
249
250        This will look in the CVS subfolder of 'folder' for
251        a file named 'Entries' and filter all elements from
252        the 'files' list that are not listed in 'Entries'.
253        """
254
255        join = os.path.join
256        cvsFiles = getCVSEntries(folder)
257        if cvsFiles:
258            indicesToDelete = []
259            for i in range(len(files)):
260                f = files[i]
261                if join(folder, f) not in cvsFiles:
262                    indicesToDelete.append(i)
263            indicesToDelete.reverse()
264            for i in indicesToDelete:
265                del files[i]
266
267        return files
268
269
270# An experimental untested base class with additional 'security'.
271
272class SecureTestCase(unittest.TestCase):
273    """Secure testing base class with additional pre- and postconditions.
274
275    We try to ensure that each test leaves the environment it has
276    found unchanged after the test is performed, successful or not.
277
278    Currently we restore sys.path and the working directory, but more
279    of this could be added easily, like removing temporary files or
280    similar things.
281
282    Use this as a base class replacing unittest.TestCase and call
283    these methods in subclassed versions before doing your own
284    business!
285    """
286
287    def setUp(self):
288        "Remember sys.path and current working directory."
289        self._initialPath = sys.path[:]
290        self._initialWorkDir = os.getcwd()
291
292    def tearDown(self):
293        "Restore previous sys.path and working directory."
294        sys.path = self._initialPath
295        os.chdir(self._initialWorkDir)
296
297class NearTestCase(unittest.TestCase):
298    def assertNear(a,b,accuracy=1e-5):
299        if isinstance(a,(float,int)):
300            if abs(a-b)>accuracy:
301                raise AssertionError("%s not near %s" % (a, b))
302        else:
303            for ae,be in zip(a,b):
304                if abs(ae-be)>accuracy:
305                    raise AssertionError("%s not near %s" % (a, b))
306    assertNear = staticmethod(assertNear)
307
308class ScriptThatMakesFileTest(unittest.TestCase):
309    """Runs a Python script at OS level, expecting it to produce a file.
310
311    It CDs to the working directory to run the script."""
312    def __init__(self, scriptDir, scriptName, outFileName, verbose=0):
313        self.scriptDir = scriptDir
314        self.scriptName = scriptName
315        self.outFileName = outFileName
316        self.verbose = verbose
317        # normally, each instance is told which method to run)
318        unittest.TestCase.__init__(self)
319
320    def setUp(self):
321        self.cwd = os.getcwd()
322        global testsFolder
323        scriptDir=self.scriptDir
324        if not os.path.isabs(scriptDir):
325            scriptDir=os.path.join(testsFolder,scriptDir)
326
327        os.chdir(scriptDir)
328        assert os.path.isfile(self.scriptName), "Script %s not found!" % self.scriptName
329        if os.path.isfile(self.outFileName):
330            os.remove(self.outFileName)
331
332    def tearDown(self):
333        os.chdir(self.cwd)
334
335    def runTest(self):
336        fmt = sys.platform=='win32' and '"%s" %s' or '%s %s'
337        import subprocess
338        out = subprocess.check_output((sys.executable,self.scriptName))
339        #p = os.popen(fmt % (sys.executable,self.scriptName),'r')
340        #out = p.read()
341        if self.verbose:
342            print(out)
343        #status = p.close()
344        assert os.path.isfile(self.outFileName), "File %s not created!" % self.outFileName
345
346def equalStrings(a,b,enc='utf8'):
347    return a==b if type(a)==type(b) else asUnicode(a,enc)==asUnicode(b,enc)
348
349def eqCheck(r,x):
350    if r!=x:
351        print('Strings unequal\nexp: %s\ngot: %s' % (ascii(x),ascii(r)))
352