Merge pull request #245 from afh/pull/DocTests

Make more examples from documentation testable
This commit is contained in:
Craig Earls 2014-02-19 09:46:53 -07:00
commit 1ec9f71479
2 changed files with 131 additions and 49 deletions

View file

@ -56,7 +56,8 @@
@c the documentation itself, in that case the journal example data @c the documentation itself, in that case the journal example data
@c needs to be specially marked as well using @smallexample @c input:UUID, @c needs to be specially marked as well using @smallexample @c input:UUID,
@c again with the UUID being the UUID of the corresponding ledger example @c again with the UUID being the UUID of the corresponding ledger example
@c command, e.g.: @c command. If multiple inputs with the same UUID are found they will be
@c concatenated together and given as one set of data to the example command.
@c @c
@c @smallexample @c input:35CB2A3 @c @smallexample @c input:35CB2A3
@c 2014/02/09 The Italian Place @c 2014/02/09 The Italian Place
@ -72,7 +73,19 @@
@c Assets:Cash @c Assets:Cash
@c Expenses:Food:Dining @c Expenses:Food:Dining
@c @end smallexample @c @end smallexample
@c @c
@c To use different example commands with the same input from the documentation
@c add with_input:UUID to the example command, where UUID is the UUID of the input,
@c e.g.:
@c
@c @smallexample @c command:94FD2B6,with_input:35CB2A3
@c $ ledger -f inline.dat bal expenses
@c @end smallexample
@c
@c @smallexample @c output:94FD2B6
@c $ 36.84 Expenses:Food:Dining
@c @end smallexample
@c
@c Additionally DocTests.py will pass --init-file /dev/null to ledger to @c Additionally DocTests.py will pass --init-file /dev/null to ledger to
@c ignore any default arguments to ledger the user running the tests @c ignore any default arguments to ledger the user running the tests
@c has configured. @c has configured.
@ -306,7 +319,7 @@ And just for the sake of example---as a starting point for those who
want to dive in head-first---here are the journal transactions from want to dive in head-first---here are the journal transactions from
above, formatted as the Ledger program wishes to see them: above, formatted as the Ledger program wishes to see them:
@smallexample @smallexample @c input:48DDF26
2004/09/29 Pacific Bell 2004/09/29 Pacific Bell
Expenses:Pacific Bell $23.00 Expenses:Pacific Bell $23.00
Assets:Checking Assets:Checking
@ -315,12 +328,37 @@ above, formatted as the Ledger program wishes to see them:
The account balances and registers in this file, if saved as The account balances and registers in this file, if saved as
@file{ledger.dat}, could be reported using: @file{ledger.dat}, could be reported using:
@smallexample @smallexample @c command:48DDF26
$ ledger -f ledger.dat balance $ ledger -f ledger.dat balance
@end smallexample
@smallexample @c output:48DDF26
$-23.00 Assets:Checking
$23.00 Expenses:Pacific Bell
--------------------
0
@end smallexample
Or
@smallexample @c command:8C7295F,with_input:48DDF26
$ ledger -f ledger.dat register checking $ ledger -f ledger.dat register checking
@end smallexample
@smallexample @c output:8C7295F
04-Sep-29 Pacific Bell Assets:Checking $-23.00 $-23.00
@end smallexample
And even:
@smallexample @c command:BB32EF2,with_input:48DDF26
$ ledger -f ledger.dat register Bell $ ledger -f ledger.dat register Bell
@end smallexample @end smallexample
@smallexample @c output:BB32EF2
04-Sep-29 Pacific Bell Expenses:Pacific Bell $23.00 $23.00
@end smallexample
An important difference between Ledger and other finance packages is An important difference between Ledger and other finance packages is
that Ledger will never alter your input file. You can create and edit that Ledger will never alter your input file. You can create and edit
that file in any way you prefer, but Ledger is only for analyzing the that file in any way you prefer, but Ledger is only for analyzing the
@ -548,7 +586,7 @@ If you would like to find transaction to only a certain payee use
$ ledger -f drewr3.dat register payee "Organic" $ ledger -f drewr3.dat register payee "Organic"
@end smallexample @end smallexample
@smallexample @c output:C10BC57E @smallexample @c output:C6BC57E
10-Dec-20 Organic Co-op Expense:Food:Groceries $ 37.50 $ 37.50 10-Dec-20 Organic Co-op Expense:Food:Groceries $ 37.50 $ 37.50
Expense:Food:Groceries $ 37.50 $ 75.00 Expense:Food:Groceries $ 37.50 $ 75.00
Expense:Food:Groceries $ 37.50 $ 112.50 Expense:Food:Groceries $ 37.50 $ 112.50
@ -700,7 +738,7 @@ owe. ``Liabilities'' is just a more inclusive name for Debts.
An Asset is typically increased by transferring money from an Income An Asset is typically increased by transferring money from an Income
account, such as when you get paid. Here is a typical transaction: account, such as when you get paid. Here is a typical transaction:
@smallexample @smallexample @c input:6B43DD4
2004/09/29 My Employer 2004/09/29 My Employer
Assets:Checking $500.00 Assets:Checking $500.00
Income:Salary Income:Salary
@ -715,7 +753,7 @@ borrow money to buy something, or if you owe someone money. Here is
an example of increasing a MasterCard liability by spending money with an example of increasing a MasterCard liability by spending money with
it: it:
@smallexample @smallexample @c input:6B43DD4
2004/09/30 Restaurant 2004/09/30 Restaurant
Expenses:Dining $25.00 Expenses:Dining $25.00
Liabilities:MasterCard Liabilities:MasterCard
@ -729,10 +767,17 @@ offsets the value of your assets.
The combined total of your Assets and Liabilities is your net worth. The combined total of your Assets and Liabilities is your net worth.
So to see your current net worth, use this command: So to see your current net worth, use this command:
@smallexample @smallexample @c command:6B43DD4
$ ledger balance ^assets ^liabilities $ ledger balance ^assets ^liabilities
@end smallexample @end smallexample
@smallexample @c output:6B43DD4
$500.00 Assets:Checking
$-25.00 Liabilities:MasterCard
--------------------
$475.00
@end smallexample
In a similar vein, your Income accounts show up negative, because they In a similar vein, your Income accounts show up negative, because they
transfer money @emph{from} an account in order to increase your transfer money @emph{from} an account in order to increase your
assets. Your Expenses show up positive because that is where the assets. Your Expenses show up positive because that is where the
@ -741,10 +786,17 @@ flow. A positive cash flow means you are spending more than you make,
since income is always a negative figure. To see your current cash since income is always a negative figure. To see your current cash
flow, use this command: flow, use this command:
@smallexample @smallexample @c command:DB128F3,with_input:6B43DD4
$ ledger balance ^income ^expenses $ ledger balance ^income ^expenses
@end smallexample @end smallexample
@smallexample @c output:DB128F3
$25.00 Expenses:Dining
$-500.00 Income:Salary
--------------------
$-475.00
@end smallexample
Another common question to ask of your expenses is: How much do I Another common question to ask of your expenses is: How much do I
spend each month on X? Ledger provides a simple way of displaying spend each month on X? Ledger provides a simple way of displaying
monthly totals for any account. Here is an example that summarizes monthly totals for any account. Here is an example that summarizes
@ -1820,7 +1872,7 @@ function on a transaction-wide or per-posting basis.
Lastly, you can specify the valuation function/value for any specific Lastly, you can specify the valuation function/value for any specific
amount using the @samp{(( ))} commodity annotation. amount using the @samp{(( ))} commodity annotation.
@smallexample @smallexample @c input:814A366
2012-03-02 KFC 2012-03-02 KFC
Expenses:Food2 $1 ((2 EUR)) Expenses:Food2 $1 ((2 EUR))
Assets:Cash2 Assets:Cash2
@ -1856,20 +1908,24 @@ amount using the @samp{(( ))} commodity annotation.
Assets:Cash9 Assets:Cash9
@end smallexample @end smallexample
@smallexample @smallexample @c command:814A366
ledger reg -V food $ ledger reg -V food
@end smallexample
@smallexample @c output:814A366
12-Mar-02 KFC Expenses:Food2 2 EUR 2 EUR 12-Mar-02 KFC Expenses:Food2 2 EUR 2 EUR
12-Mar-03 KFC <Adjustment> -1 EUR 1 EUR 12-Mar-03 KFC Expenses:Food3 3 EUR 5 EUR
Expenses:Food3 3 EUR 4 EUR 12-Mar-04 KFC Expenses:Food4 4 EUR 9 EUR
12-Mar-04 KFC <Adjustment> -2 EUR 2 EUR 12-Mar-05 KFC Expenses:Food5 $1 $1
Expenses:Food4 4 EUR 6 EUR 9 EUR
12-Mar-05 KFC <Adjustment> -3 EUR 3 EUR 12-Mar-06 KFC Expenses:Food6 $1 $2
Expenses:Food5 5 EUR 8 EUR 9 EUR
12-Mar-06 KFC <Adjustment> -4 EUR 4 EUR 12-Mar-07 KFC Expenses:Food7 1 CAD $2
Expenses:Food6 6 EUR 10 EUR 1 CAD
12-Mar-07 KFC Expenses:Food7 7 EUR 17 EUR 9 EUR
12-Mar-08 XACT Expenses:Food8 8 EUR 25 EUR 12-Mar-08 XACT Expenses:Food8 $1 $3
12-Mar-09 POST (Expenses:Food9) 9 EUR 34 EUR 1 CAD
9 EUR
@end smallexample @end smallexample
@node Keeping it Consistent, Journal Format, Currency and Commodities, Keeping a Journal @node Keeping it Consistent, Journal Format, Currency and Commodities, Keeping a Journal

View file

@ -22,6 +22,7 @@ class DocTests:
self.testin_token = 'command' self.testin_token = 'command'
self.testout_token = 'output' self.testout_token = 'output'
self.testdat_token = 'input' self.testdat_token = 'input'
self.testwithdat_token = 'with_input'
def read_example(self): def read_example(self):
endexample = re.compile(r'^@end\s+smallexample\s*$') endexample = re.compile(r'^@end\s+smallexample\s*$')
@ -37,7 +38,7 @@ class DocTests:
return hashlib.sha1(example.rstrip()).hexdigest()[0:7].upper() return hashlib.sha1(example.rstrip()).hexdigest()[0:7].upper()
def find_examples(self): def find_examples(self):
startexample = re.compile(r'^@smallexample\s+@c\s+(%s|%s|%s)(?::([\dA-Fa-f]+))?' startexample = re.compile(r'^@smallexample\s+@c\s+(%s|%s|%s)(?::([\dA-Fa-f]+))?(?:,(.*))?'
% (self.testin_token, self.testout_token, self.testdat_token)) % (self.testin_token, self.testout_token, self.testdat_token))
while True: while True:
line = self.file.readline() line = self.file.readline()
@ -50,6 +51,13 @@ class DocTests:
test_begin_line = self.current_line test_begin_line = self.current_line
test_kind = startmatch.group(1) test_kind = startmatch.group(1)
test_id = startmatch.group(2) test_id = startmatch.group(2)
test_options = dict()
for pair in re.split(r',\s*', str(startmatch.group(3))):
kv = re.split(r':\s*', pair, 2)
try:
test_options[kv[0]] = kv[1]
except IndexError:
pass
example = self.read_example() example = self.read_example()
test_end_pos = self.file.tell() test_end_pos = self.file.tell()
test_end_line = self.current_line test_end_line = self.current_line
@ -68,22 +76,52 @@ class DocTests:
except KeyError: except KeyError:
self.examples[test_id] = dict() self.examples[test_id] = dict()
try:
example = self.examples[test_id][test_kind][test_kind] + example
except KeyError:
pass
self.examples[test_id][test_kind] = { self.examples[test_id][test_kind] = {
'bpos': test_begin_pos, 'bpos': test_begin_pos,
'epos': test_end_pos, 'epos': test_end_pos,
'blin': test_begin_line, 'blin': test_begin_line,
'elin': test_end_line, 'elin': test_end_line,
'opts': test_options,
test_kind: example, test_kind: example,
} }
def parse_command(self, test_id, example):
try:
command = example[self.testin_token][self.testin_token]
except KeyError:
return None
command = command.rstrip().split()
if command[0] == '$': command.remove('$')
index = command.index('ledger')
command[index] = self.ledger
command.insert(index+1, '--init-file')
command.insert(index+2, '/dev/null')
try:
findex = command.index('-f')
except ValueError:
try:
findex = command.index('--file')
except ValueError:
findex = index+1
command.insert(findex, '--file')
command.insert(findex+1, test_id + '.dat')
return (command, findex+1)
def test_examples(self): def test_examples(self):
failed = set() failed = set()
for test_id in self.examples: for test_id in self.examples:
example = self.examples[test_id] example = self.examples[test_id]
try: try:
command = example[self.testin_token][self.testin_token] (command, findex) = self.parse_command(test_id, example)
except KeyError: except TypeError:
command = None failed.add(test_id)
continue
try: try:
output = example[self.testout_token][self.testout_token] output = example[self.testout_token][self.testout_token]
@ -93,44 +131,32 @@ class DocTests:
try: try:
input = example[self.testdat_token][self.testdat_token] input = example[self.testdat_token][self.testdat_token]
except KeyError: except KeyError:
input = None try:
with_input = example[self.testin_token]['opts'][self.testwithdat_token]
input = self.examples[with_input][self.testdat_token][self.testdat_token]
except KeyError:
input = None
if command and output: if command and output:
command = command.rstrip().split() test_file_created = False
if command[0] == '$': command.remove('$')
index = command.index('ledger')
command[index] = self.ledger
command.insert(index+1, '--init-file')
command.insert(index+2, '/dev/null')
try:
findex = command.index('-f')
except ValueError:
try:
findex = command.index('--file')
except ValueError:
findex = index+1
command.insert(findex, '--file')
command.insert(findex+1, test_id + '.dat')
if findex: if findex:
scriptpath = os.path.dirname(os.path.realpath(__file__)) scriptpath = os.path.dirname(os.path.realpath(__file__))
test_input_dir = scriptpath + '/../test/input/' test_input_dir = scriptpath + '/../test/input/'
test_file = command[findex+1] test_file = command[findex]
test_file_created = False
if not os.path.exists(test_file): if not os.path.exists(test_file):
if input: if input:
test_file_created = True test_file_created = True
with open(test_file, 'w') as f: with open(test_file, 'w') as f:
f.write(input) f.write(input)
elif os.path.exists(test_input_dir + test_file): elif os.path.exists(test_input_dir + test_file):
command[findex+1] = test_input_dir + test_file command[findex] = test_input_dir + test_file
try: try:
verify = subprocess.check_output(command) verify = subprocess.check_output(command)
except: except:
verify = str() verify = str()
if test_file_created:
os.remove(test_file)
valid = (output == verify) valid = (output == verify)
if valid and test_file_created:
os.remove(test_file)
if self.verbose > 0: if self.verbose > 0:
print test_id, ':', 'Passed' if valid else 'FAILED' print test_id, ':', 'Passed' if valid else 'FAILED'
else: else: