Skip to content

Commit d9e0c2b

Browse files
committed
Initial commit of test cases
1 parent baa1d95 commit d9e0c2b

File tree

5 files changed

+195
-1
lines changed

5 files changed

+195
-1
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ config.log
66
config.status
77
description-pak
88
doc-pak/
9+
oldtest/
910
src/.deps/
1011
src/pgdbf
1112
src/pgdbf.o
1213
stamp-h1
13-
test/
14+
test/privatecases/
1415
*.o
1516
.DS_Store
1617
*.tar.xz

test/README.md

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Creating test cases
2+
3+
Publicly distributable test cases go in the `test/cases` directory. Put private and/or confidential cases in `test/privatecases`, which Git ignores.
4+
5+
Within those directories, place your database files into the `data` subdirectory.
6+
7+
Test cases are JSON files named `[something].json` with defined keys:
8+
9+
* **cmd_args**: the arguments passed to pgdbf to generate the test output. If this is a string, it will be passed as a single argument to pgdbf. If you're attempting to pass more than one argument, *this is probably not what you want*. In that case, set `cmd_args` to a list of string arguments.
10+
* With one argument: `"cmd_args": "mytable.dbf"`
11+
* With multiple arguments: `"cmd_args": ["-m", "memofile.fpt", "datafile.dbf"]`
12+
* **head**: a string to be matched against the start of the test output
13+
* **md5**: the expected MD5 hex digest of the test output
14+
* **tail**: a string to be matched against the start of the test output
15+
16+
Unknown keys are ignored.
17+
18+
Public test cases are loaded and executed in alphabetical order, followed by private test cases.
19+
20+
# Running test cases
21+
22+
Run the `runtests.py` program *from within the `test/` directory* to execute all unit tests.
23+
24+
If you specify the `-p` argument to choose which pgdbf executable to test, use either an absolute path, or path *relative to being inside the `cases/` or `privatecases/` directory*. `runtests.py` CDs into each directory in turn so that a test case's `cmd_args` value can refer to a relative path like `data/myfile.dbf`, so giving a path relative to `test/` will fail.
25+
26+
For example:
27+
28+
./runtests.py ../../src/pgdbf
29+
30+
will test a freshly built-but-not-installed binary.

test/cases/data/.keepme

Whitespace-only changes.

test/privatecases/data/.keepme

Whitespace-only changes.

test/runtests.py

+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
#!/usr/bin/env python
2+
3+
# pylint: disable=superfluous-parens
4+
5+
"""Run a suite of PgDBF test cases."""
6+
7+
import argparse
8+
from glob import glob
9+
from hashlib import md5
10+
from json import load
11+
from logging import basicConfig, getLogger, DEBUG, INFO
12+
from os import chdir, getcwd
13+
from subprocess import Popen, PIPE, STDOUT
14+
15+
LOGGER = getLogger('')
16+
17+
class TestError(ValueError):
18+
"""A test failed"""
19+
20+
21+
def check_head(expected):
22+
"""Check that the start of the file is as expected"""
23+
24+
LOGGER.debug('opened a header check')
25+
body = ''
26+
length = len(expected)
27+
while True:
28+
data = yield()
29+
if not data:
30+
raise ValueError({'error': 'short read', 'expected': length, 'actual': len(body)})
31+
body += data
32+
if len(body) >= length:
33+
actual = body[:length]
34+
if expected != actual:
35+
raise TestError('unequal head', expected, actual)
36+
LOGGER.info('passed the header check')
37+
break
38+
39+
while True:
40+
data = yield()
41+
if data is None:
42+
LOGGER.debug('closed the header check')
43+
return
44+
45+
46+
def check_length(expected):
47+
"""Check that the file has the expected length"""
48+
49+
LOGGER.debug('opened a length check')
50+
actual = 0
51+
while True:
52+
data = yield()
53+
if not data:
54+
if expected != actual:
55+
raise TestError('incorrect length', expected, actual)
56+
LOGGER.info('passed the length check')
57+
LOGGER.debug('closed the length check')
58+
return
59+
actual += len(data)
60+
61+
62+
def check_md5(expected):
63+
"""Check that the file has the expected MD5 hash"""
64+
65+
LOGGER.debug('opened an md5 check')
66+
hasher = md5()
67+
while True:
68+
data = yield()
69+
if data is None:
70+
actual = hasher.hexdigest()
71+
if expected != actual:
72+
raise TestError('bad md5 hash', actual, expected)
73+
LOGGER.info('passed the md5 check')
74+
LOGGER.debug('closed the md5 check')
75+
return
76+
hasher.update(data)
77+
78+
79+
def check_tail(expected):
80+
"""Check that the end of the file is as expected"""
81+
82+
LOGGER.debug('opened a tail check')
83+
actual = ''
84+
length = len(expected)
85+
while True:
86+
data = yield()
87+
if data is None:
88+
if expected != actual:
89+
raise TestError('incorrect tail', actual, expected)
90+
LOGGER.info('passed the tail check')
91+
LOGGER.debug('closed the tail check')
92+
return
93+
actual = (actual + data)[-length:]
94+
95+
96+
def run_test(pgdbf_path, config):
97+
"""Run a test case with the given pgdbf executable"""
98+
99+
tests = []
100+
for key, value in config.items():
101+
try:
102+
test_func = {
103+
'head': check_head,
104+
'length': check_length,
105+
'md5': check_md5,
106+
'tail': check_tail,
107+
}[key]
108+
except KeyError:
109+
pass
110+
else:
111+
test = test_func(value)
112+
next(test)
113+
tests.append(test)
114+
115+
if not tests:
116+
raise ValueError('No tests are configured')
117+
118+
args = config['cmd_args']
119+
if not isinstance(args, list):
120+
args = [args]
121+
command = Popen([pgdbf_path] + args, stdout=PIPE, stderr=STDOUT)
122+
while True:
123+
chunk = command.stdout.read(128 * 1024)
124+
if not chunk:
125+
break
126+
for test in tests:
127+
test.send(chunk)
128+
129+
for test in tests:
130+
try:
131+
test.send(None)
132+
except StopIteration:
133+
pass
134+
else:
135+
raise ValueError('test {} did not close cleanly'.format(test))
136+
137+
138+
def handle_command_line():
139+
"""Evaluate the command line arguments and run tests"""
140+
141+
parser = argparse.ArgumentParser(description=__doc__)
142+
parser.add_argument('--pgdbf', '-p', help='Path to the pgdbf executable')
143+
parser.add_argument('--verbose', '-v', action='count', help='Increase debugging verbosity')
144+
args = parser.parse_args()
145+
if args.verbose >= 2:
146+
basicConfig(level=DEBUG)
147+
elif args.verbose == 1:
148+
basicConfig(level=INFO)
149+
else:
150+
basicConfig()
151+
152+
orig_dir = getcwd()
153+
for test_dir in ('cases', 'privatecases'):
154+
chdir(test_dir)
155+
for case in glob('*.json'):
156+
print('Running {}/{}'.format(test_dir, case))
157+
with open(case) as infile:
158+
run_test(args.pgdbf or 'pgdbf', load(infile))
159+
chdir(orig_dir)
160+
161+
162+
if __name__ == '__main__':
163+
handle_command_line()

0 commit comments

Comments
 (0)