From fbbb379fe08b051b40c071041108a2526533f417 Mon Sep 17 00:00:00 2001 From: Alexis Hildebrandt Date: Fri, 7 Feb 2014 00:21:38 +0100 Subject: Check examples in documentation when running tests The DocTests.py script will parse a given texinfo file for specially marked examples, run the ledger command from the example, and check the result against the example output from the documentation. --- test/DocTests.py | 139 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100755 test/DocTests.py (limited to 'test/DocTests.py') diff --git a/test/DocTests.py b/test/DocTests.py new file mode 100755 index 00000000..daac1db5 --- /dev/null +++ b/test/DocTests.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import os +import re +import sys +import hashlib +import subprocess + +from difflib import unified_diff + +class DocTests: + def __init__(self, argv): + if not os.path.isfile(argv[1]): + print "Cannot find ledger at '%s'" % argv[1] + sys.exit(1) + if not os.path.isfile(argv[2]): + print "Cannot find source path at '%s'" % argv[2] + sys.exit(1) + + self.ledger = os.path.abspath(argv[1]) + self.sourcepath = os.path.abspath(argv[2]) + scriptpath = os.path.dirname(os.path.realpath(__file__)) + self.verbose = False + self.debug = False + self.examples = dict() + self.testin_token = 'command' + self.testout_token = 'output' + + def read_example(self): + endexample = re.compile(r'^@end\s+smallexample\s*$') + example = str() + while True: + line = self.file.readline() + self.current_line += 1 + if len(line) <= 0 or endexample.match(line): break + example += line + return example + + def test_id(self, example): + return hashlib.sha1(example.rstrip()).hexdigest()[0:7].upper() + + def find_examples(self): + startexample = re.compile(r'^@smallexample\s+@c\s+(%s|%s)(?::([\dA-Fa-f]+))?' % (self.testin_token, self.testout_token)) + while True: + line = self.file.readline() + self.current_line += 1 + if len(line) <= 0: break + + startmatch = startexample.match(line) + if (startmatch): + test_begin_pos = self.file.tell() + test_begin_line = self.current_line + test_kind = startmatch.group(1) + test_id = startmatch.group(2) + example = self.read_example() + test_end_pos = self.file.tell() + test_end_line = self.current_line + + if not test_id: + print >> sys.stderr, 'Example', test_kind, 'in line', test_begin_line, 'is missing id.' + test_id = self.test_id(example) + if test_kind == self.testin_token: + print >> sys.stderr, 'Use', self.test_id(example) + elif test_kind == self.testin_token and test_id != self.test_id(example): + print >> sys.stderr, 'Expected test id', test_id, 'for example' \ + , test_kind, 'on line', test_begin_line, 'to be', self.test_id(example) + + try: + self.examples[test_id] + except KeyError: + self.examples[test_id] = dict() + + self.examples[test_id][test_kind] = { + 'bpos': test_begin_pos, + 'epos': test_end_pos, + 'blin': test_begin_line, + 'elin': test_end_line, + test_kind: example, + } + + def test_examples(self): + failed = 0 + for test_id in self.examples: + example = self.examples[test_id] + try: + command = example[self.testin_token][self.testin_token] + except KeyError: + command = None + try: + output = example[self.testout_token][self.testout_token] + except KeyError: + output = None + + if command and output: + 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') + scriptpath = os.path.dirname(os.path.realpath(__file__)) + test_input_dir = scriptpath + '/../test/input/' + for i, arg in enumerate(command): + if '.dat' in arg or '.ledger' in arg: + if os.path.exists(test_input_dir + arg): + command[i] = test_input_dir + arg + try: + verify = subprocess.check_output(command)#.decode('utf-8') + except: + verify = str() + valid = (output == verify) + if self.verbose: + print test_id, ':', u'Passed' if valid else u'FAILED' + else: + sys.stdout.write('.' if valid else 'E') + + if not valid: + failed += 1 + if self.debug: + print ' '.join(command) + for line in unified_diff(output.split('\n'), verify.split('\n'), fromfile='generated', tofile='expected'): + print(line) + print + print + return failed + + def main(self): + self.file = open(self.sourcepath) + self.current_line = 0 + self.find_examples() + failed_examples = self.test_examples() + self.file.close() + return failed_examples + +if __name__ == "__main__": + script = DocTests(sys.argv) + status = script.main() + sys.exit(status) -- cgit v1.2.3 From a1cc8ca15ae1761fbcb4c6ec9f8af82e8ab411ab Mon Sep 17 00:00:00 2001 From: Alexis Hildebrandt Date: Fri, 7 Feb 2014 18:38:43 +0100 Subject: Add support to check documentation examples with inline data --- doc/ledger3.texi | 58 ++++++++++++++++++++++++++++++++------------------------ test/DocTests.py | 45 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 69 insertions(+), 34 deletions(-) (limited to 'test/DocTests.py') diff --git a/doc/ledger3.texi b/doc/ledger3.texi index acf58698..48fafc13 100644 --- a/doc/ledger3.texi +++ b/doc/ledger3.texi @@ -1503,7 +1503,7 @@ entry. For example, the following entries reflect transaction made for a business trip to Europe from the US: -@smallexample +@smallexample @c input:82150D9 2011/09/23 Cash in Munich Assets:Cash E50.00 Assets:Checking $-66.00 @@ -1519,8 +1519,11 @@ spent on Dinner in Munich. Running a ledger balance report shows: -@smallexample +@smallexample @c command:82150D9 $ ledger -f example.dat bal +@end smallexample + +@smallexample @c output:82150D9 $-66.00 E15.00 Assets E15.00 Cash @@ -3641,7 +3644,7 @@ the money to be evenly distributed over the next six months so that your monthly budgets gradually take a hit for the vegetables you'll pick up from the co-op, even though you've already paid for them. -@smallexample +@smallexample @c input:6453542 2008/10/16 * (2090) Bountiful Blessings Farm Expenses:Food:Groceries $ 37.50 ; [=2008/10/01] Expenses:Food:Groceries $ 37.50 ; [=2008/11/01] @@ -3659,15 +3662,17 @@ really knows that it debited $225 this month. And using @option{--effective} option, initial date will be overridden by effective dates. -@smallexample +@smallexample @c command:6453542 $ ledger --effective register Groceries +@end smallexample -08-Oct-01 Bountiful Blessi.. Expe:Food:Groceries $ 37.50 $ 37.50 -08-Nov-01 Bountiful Blessi.. Expe:Food:Groceries $ 37.50 $ 75.00 -08-Dec-01 Bountiful Blessi.. Expe:Food:Groceries $ 37.50 $ 112.50 -09-Jan-01 Bountiful Blessi.. Expe:Food:Groceries $ 37.50 $ 150.00 -09-Feb-01 Bountiful Blessi.. Expe:Food:Groceries $ 37.50 $ 187.50 -09-Mar-01 Bountiful Blessi.. Expe:Food:Groceries $ 37.50 $ 225.00 +@smallexample @c output:6453542 +08-Oct-01 Bountiful Blessings.. Expense:Food:Groceries $ 37.50 $ 37.50 +08-Nov-01 Bountiful Blessings.. Expense:Food:Groceries $ 37.50 $ 75.00 +08-Dec-01 Bountiful Blessings.. Expense:Food:Groceries $ 37.50 $ 112.50 +09-Jan-01 Bountiful Blessings.. Expense:Food:Groceries $ 37.50 $ 150.00 +09-Feb-01 Bountiful Blessings.. Expense:Food:Groceries $ 37.50 $ 187.50 +09-Mar-01 Bountiful Blessings.. Expense:Food:Groceries $ 37.50 $ 225.00 @end smallexample @node Periodic Transactions, Concrete Example of Automated Transactions, Effective Dates, Automated Transactions @@ -3792,14 +3797,14 @@ options. The balance report is the most commonly used report. The simplest invocation is: -@smallexample +@smallexample @c command:1D00D56 $ ledger balance -f drewr3.dat @end smallexample @noindent which will print the balances of every account in your journal. -@smallexample +@smallexample @c output:1D00D56 $ -3,804.00 Assets $ 1,396.00 Checking $ 30.00 Business @@ -3826,8 +3831,11 @@ Most times this is more than you want. Limiting the results to specific accounts is as easy as entering the names of the accounts after the command. -@smallexample +@smallexample @c command:06B2AD4 $ ledger balance -f drewr3.dat Auto MasterCard +@end smallexample + +@smallexample @c output:06B2AD4 $ 5,500.00 Expenses:Auto $ -20.00 Liabilities:MasterCard -------------------- @@ -4992,7 +5000,7 @@ earlier postings. Here's how it works: Say you currently have this posting in your ledger file: -@smallexample +@smallexample @c input:03ACB97 2004/03/15 * Viva Italiano Expenses:Food $12.45 Expenses:Tips $2.55 @@ -5003,17 +5011,17 @@ Now it's @samp{2004/4/9}, and you've just eating at @samp{Viva Italiano} again. The exact amounts are different, but the overall form is the same. With the @command{xact} command you can type: -@smallexample +@smallexample @c command:03ACB97 $ ledger xact 2004/4/9 viva food 11 tips 2.50 @end smallexample This produces the following output: -@smallexample +@smallexample @c output:03ACB97 2004/04/09 Viva Italiano - Expenses:Food $11.00 - Expenses:Tips $2.50 - Liabilities:MasterCard $-13.50 + Expenses:Food $11.00 + Expenses:Tips $2.50 + Liabilities:MasterCard @end smallexample It works by finding a past posting matching the regular expression @@ -6491,7 +6499,7 @@ In the balance report, it shows all the accounts affected by transactions having a related posting. For example, if a file had this transaction: -@smallexample +@smallexample @c input:94C5675 2004/03/20 Safeway Expenses:Food $65.00 Expenses:Cash $20.00 @@ -6500,16 +6508,16 @@ this transaction: And the register command was: -@smallexample -$ ledger -r register food +@smallexample @c command:94C5675 +$ ledger -f example.dat -r register food @end smallexample The following would be output, showing the postings related to the posting that matched: -@smallexample -2004/03/20 Safeway Expenses:Cash $-20.00 $-20.00 - Assets:Checking $85.00 $65.00 +@smallexample @c output:94C5675 +04-Mar-20 Safeway Expenses:Cash $20.00 $20.00 + Assets:Checking $-85.00 $-65.00 @end smallexample @item --budget diff --git a/test/DocTests.py b/test/DocTests.py index daac1db5..eb1a0205 100755 --- a/test/DocTests.py +++ b/test/DocTests.py @@ -26,6 +26,8 @@ class DocTests: self.examples = dict() self.testin_token = 'command' self.testout_token = 'output' + self.testdat_token = 'input' + self.test_files = list() def read_example(self): endexample = re.compile(r'^@end\s+smallexample\s*$') @@ -41,7 +43,8 @@ class DocTests: return hashlib.sha1(example.rstrip()).hexdigest()[0:7].upper() def find_examples(self): - startexample = re.compile(r'^@smallexample\s+@c\s+(%s|%s)(?::([\dA-Fa-f]+))?' % (self.testin_token, self.testout_token)) + startexample = re.compile(r'^@smallexample\s+@c\s+(%s|%s|%s)(?::([\dA-Fa-f]+))?' + % (self.testin_token, self.testout_token, self.testdat_token)) while True: line = self.file.readline() self.current_line += 1 @@ -87,11 +90,17 @@ class DocTests: command = example[self.testin_token][self.testin_token] except KeyError: command = None + try: output = example[self.testout_token][self.testout_token] except KeyError: output = None + try: + input = example[self.testdat_token][self.testdat_token] + except KeyError: + input = None + if command and output: command = command.rstrip().split() if command[0] == '$': command.remove('$') @@ -99,19 +108,37 @@ class DocTests: command[index] = self.ledger command.insert(index+1, '--init-file') command.insert(index+2, '/dev/null') - scriptpath = os.path.dirname(os.path.realpath(__file__)) - test_input_dir = scriptpath + '/../test/input/' - for i, arg in enumerate(command): - if '.dat' in arg or '.ledger' in arg: - if os.path.exists(test_input_dir + arg): - command[i] = test_input_dir + arg try: - verify = subprocess.check_output(command)#.decode('utf-8') + 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: + scriptpath = os.path.dirname(os.path.realpath(__file__)) + test_input_dir = scriptpath + '/../test/input/' + test_file = command[findex+1] + test_file_created = False + if not os.path.exists(test_file): + if input: + test_file_created = True + with open(test_file, 'w') as f: + f.write(input) + elif os.path.exists(test_input_dir + test_file): + command[findex+1] = test_input_dir + test_file + try: + verify = subprocess.check_output(command) except: verify = str() + if test_file_created: + os.remove(test_file) valid = (output == verify) if self.verbose: - print test_id, ':', u'Passed' if valid else u'FAILED' + print test_id, ':', 'Passed' if valid else 'FAILED' else: sys.stdout.write('.' if valid else 'E') -- cgit v1.2.3 From 960ebc2a572e3ff90d2b95c54f618430df5db351 Mon Sep 17 00:00:00 2001 From: Alexis Hildebrandt Date: Sun, 9 Feb 2014 07:20:03 +0100 Subject: Print summary list of failed doc tests if any --- test/DocTests.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'test/DocTests.py') diff --git a/test/DocTests.py b/test/DocTests.py index eb1a0205..736be6c7 100755 --- a/test/DocTests.py +++ b/test/DocTests.py @@ -83,7 +83,7 @@ class DocTests: } def test_examples(self): - failed = 0 + failed = set() for test_id in self.examples: example = self.examples[test_id] try: @@ -143,14 +143,17 @@ class DocTests: sys.stdout.write('.' if valid else 'E') if not valid: - failed += 1 if self.debug: + failed.add(test_id) print ' '.join(command) for line in unified_diff(output.split('\n'), verify.split('\n'), fromfile='generated', tofile='expected'): print(line) print print - return failed + if len(failed) > 0: + print "\nThe following examples failed:" + print " ", "\n ".join(failed) + return len(failed) def main(self): self.file = open(self.sourcepath) -- cgit v1.2.3 From c566afe3b1b24d3efea0e14c17a45d0987f42f16 Mon Sep 17 00:00:00 2001 From: Alexis Hildebrandt Date: Sun, 9 Feb 2014 07:28:58 +0100 Subject: Add proper argument parsing to DocTests.py --- test/CMakeLists.txt | 2 +- test/DocTests.py | 56 ++++++++++++++++++++++++++++++++++------------------- 2 files changed, 37 insertions(+), 21 deletions(-) (limited to 'test/DocTests.py') diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 159ab5be..796ef0a2 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -45,7 +45,7 @@ if(PYTHONINTERP_FOUND) get_filename_component(TestFile_Name ${TestFile} NAME_WE) add_test(${_class}Test_${TestFile_Name} ${PYTHON_EXECUTABLE} ${PROJECT_SOURCE_DIR}/test/DocTests.py - ${LEDGER_LOCATION} ${TestFile}) + --ledger ${LEDGER_LOCATION} --file ${TestFile}) set_target_properties(check PROPERTIES DEPENDS ${_class}Test_${TestFile_Name}) endforeach() endif() diff --git a/test/DocTests.py b/test/DocTests.py index 736be6c7..a50ec03d 100755 --- a/test/DocTests.py +++ b/test/DocTests.py @@ -5,29 +5,23 @@ import os import re import sys import hashlib +import argparse import subprocess from difflib import unified_diff class DocTests: - def __init__(self, argv): - if not os.path.isfile(argv[1]): - print "Cannot find ledger at '%s'" % argv[1] - sys.exit(1) - if not os.path.isfile(argv[2]): - print "Cannot find source path at '%s'" % argv[2] - sys.exit(1) - - self.ledger = os.path.abspath(argv[1]) - self.sourcepath = os.path.abspath(argv[2]) - scriptpath = os.path.dirname(os.path.realpath(__file__)) - self.verbose = False - self.debug = False - self.examples = dict() - self.testin_token = 'command' + def __init__(self, args): + scriptpath = os.path.dirname(os.path.realpath(__file__)) + self.ledger = os.path.abspath(args.ledger) + self.sourcepath = os.path.abspath(args.file) + self.verbose = args.verbose + + self.examples = dict() + self.test_files = list() + self.testin_token = 'command' self.testout_token = 'output' self.testdat_token = 'input' - self.test_files = list() def read_example(self): endexample = re.compile(r'^@end\s+smallexample\s*$') @@ -137,19 +131,20 @@ class DocTests: if test_file_created: os.remove(test_file) valid = (output == verify) - if self.verbose: + if self.verbose > 0: print test_id, ':', 'Passed' if valid else 'FAILED' else: sys.stdout.write('.' if valid else 'E') if not valid: - if self.debug: failed.add(test_id) + if self.verbose > 1: print ' '.join(command) for line in unified_diff(output.split('\n'), verify.split('\n'), fromfile='generated', tofile='expected'): print(line) print - print + if not self.verbose: + print if len(failed) > 0: print "\nThe following examples failed:" print " ", "\n ".join(failed) @@ -164,6 +159,27 @@ class DocTests: return failed_examples if __name__ == "__main__": - script = DocTests(sys.argv) + def getargs(): + parser = argparse.ArgumentParser(description='DocTests', prefix_chars='-') + parser.add_argument('-v', '--verbose', + dest='verbose', + action='count', + help='be verbose. Add -vv for more verbosity') + parser.add_argument('-l', '--ledger', + dest='ledger', + type=str, + action='store', + required=True, + help='the path to the ledger executable to test with') + parser.add_argument('-f', '--file', + dest='file', + type=str, + action='store', + required=True, + help='the texinfo documentation file to run the examples from') + return parser.parse_args() + + args = getargs() + script = DocTests(args) status = script.main() sys.exit(status) -- cgit v1.2.3 From 90988feebcd2f37c2715627d53b9c4a12dea51a5 Mon Sep 17 00:00:00 2001 From: Alexis Hildebrandt Date: Tue, 11 Feb 2014 09:47:38 +0100 Subject: DocTests: Allow multiple example inputs to be used as single ledger data for an example command --- doc/ledger3.texi | 13 ++++++++++--- test/DocTests.py | 5 +++++ 2 files changed, 15 insertions(+), 3 deletions(-) (limited to 'test/DocTests.py') diff --git a/doc/ledger3.texi b/doc/ledger3.texi index 0d337b86..ac5939aa 100644 --- a/doc/ledger3.texi +++ b/doc/ledger3.texi @@ -700,7 +700,7 @@ owe. ``Liabilities'' is just a more inclusive name for Debts. An Asset is typically increased by transferring money from an Income account, such as when you get paid. Here is a typical transaction: -@smallexample +@smallexample @c input:6B43DD4 2004/09/29 My Employer Assets:Checking $500.00 Income:Salary @@ -715,7 +715,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 it: -@smallexample +@smallexample @c input:6B43DD4 2004/09/30 Restaurant Expenses:Dining $25.00 Liabilities:MasterCard @@ -729,10 +729,17 @@ offsets the value of your assets. The combined total of your Assets and Liabilities is your net worth. So to see your current net worth, use this command: -@smallexample +@smallexample @c command:6B43DD4 $ ledger balance ^assets ^liabilities @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 transfer money @emph{from} an account in order to increase your assets. Your Expenses show up positive because that is where the diff --git a/test/DocTests.py b/test/DocTests.py index a50ec03d..60d8c637 100755 --- a/test/DocTests.py +++ b/test/DocTests.py @@ -68,6 +68,11 @@ class DocTests: except KeyError: 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] = { 'bpos': test_begin_pos, 'epos': test_end_pos, -- cgit v1.2.3 From 3d9faef448fcbf85bf565ffa9a5830a9fb67fcdd Mon Sep 17 00:00:00 2001 From: Alexis Hildebrandt Date: Sat, 15 Feb 2014 17:01:33 +0100 Subject: DocTests: Allow inline input to be used with different example commands --- doc/ledger3.texi | 89 +++++++++++++++++++++++++++++++++++++++++++------------- test/DocTests.py | 73 +++++++++++++++++++++++++++++----------------- 2 files changed, 116 insertions(+), 46 deletions(-) (limited to 'test/DocTests.py') diff --git a/doc/ledger3.texi b/doc/ledger3.texi index ac5939aa..54441188 100644 --- a/doc/ledger3.texi +++ b/doc/ledger3.texi @@ -56,7 +56,8 @@ @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 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 @smallexample @c input:35CB2A3 @c 2014/02/09 The Italian Place @@ -72,7 +73,19 @@ @c Assets:Cash @c Expenses:Food:Dining @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 ignore any default arguments to ledger the user running the tests @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 above, formatted as the Ledger program wishes to see them: -@smallexample +@smallexample @c input:48DDF26 2004/09/29 Pacific Bell Expenses:Pacific Bell $23.00 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 @file{ledger.dat}, could be reported using: -@smallexample +@smallexample @c command:48DDF26 $ 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 +@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 @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 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 @@ -548,7 +586,7 @@ If you would like to find transaction to only a certain payee use $ ledger -f drewr3.dat register payee "Organic" @end smallexample -@smallexample @c output:C10BC57E +@smallexample @c output:C6BC57E 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 $ 112.50 @@ -748,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 flow, use this command: -@smallexample +@smallexample @c command:DB128F3,with_input:6B43DD4 $ ledger balance ^income ^expenses @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 spend each month on X? Ledger provides a simple way of displaying monthly totals for any account. Here is an example that summarizes @@ -1827,7 +1872,7 @@ function on a transaction-wide or per-posting basis. Lastly, you can specify the valuation function/value for any specific amount using the @samp{(( ))} commodity annotation. -@smallexample +@smallexample @c input:814A366 2012-03-02 KFC Expenses:Food2 $1 ((2 EUR)) Assets:Cash2 @@ -1863,20 +1908,24 @@ amount using the @samp{(( ))} commodity annotation. Assets:Cash9 @end smallexample -@smallexample -ledger reg -V food +@smallexample @c command:814A366 +$ ledger reg -V food +@end smallexample + +@smallexample @c output:814A366 12-Mar-02 KFC Expenses:Food2 2 EUR 2 EUR -12-Mar-03 KFC -1 EUR 1 EUR - Expenses:Food3 3 EUR 4 EUR -12-Mar-04 KFC -2 EUR 2 EUR - Expenses:Food4 4 EUR 6 EUR -12-Mar-05 KFC -3 EUR 3 EUR - Expenses:Food5 5 EUR 8 EUR -12-Mar-06 KFC -4 EUR 4 EUR - Expenses:Food6 6 EUR 10 EUR -12-Mar-07 KFC Expenses:Food7 7 EUR 17 EUR -12-Mar-08 XACT Expenses:Food8 8 EUR 25 EUR -12-Mar-09 POST (Expenses:Food9) 9 EUR 34 EUR +12-Mar-03 KFC Expenses:Food3 3 EUR 5 EUR +12-Mar-04 KFC Expenses:Food4 4 EUR 9 EUR +12-Mar-05 KFC Expenses:Food5 $1 $1 + 9 EUR +12-Mar-06 KFC Expenses:Food6 $1 $2 + 9 EUR +12-Mar-07 KFC Expenses:Food7 1 CAD $2 + 1 CAD + 9 EUR +12-Mar-08 XACT Expenses:Food8 $1 $3 + 1 CAD + 9 EUR @end smallexample @node Keeping it Consistent, Journal Format, Currency and Commodities, Keeping a Journal diff --git a/test/DocTests.py b/test/DocTests.py index 60d8c637..cc540aa9 100755 --- a/test/DocTests.py +++ b/test/DocTests.py @@ -22,6 +22,7 @@ class DocTests: self.testin_token = 'command' self.testout_token = 'output' self.testdat_token = 'input' + self.testwithdat_token = 'with_input' def read_example(self): endexample = re.compile(r'^@end\s+smallexample\s*$') @@ -37,7 +38,7 @@ class DocTests: return hashlib.sha1(example.rstrip()).hexdigest()[0:7].upper() 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)) while True: line = self.file.readline() @@ -50,6 +51,13 @@ class DocTests: test_begin_line = self.current_line test_kind = startmatch.group(1) 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() test_end_pos = self.file.tell() test_end_line = self.current_line @@ -78,17 +86,42 @@ class DocTests: 'epos': test_end_pos, 'blin': test_begin_line, 'elin': test_end_line, + 'opts': test_options, 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): failed = set() for test_id in self.examples: example = self.examples[test_id] try: - command = example[self.testin_token][self.testin_token] - except KeyError: - command = None + (command, findex) = self.parse_command(test_id, example) + except TypeError: + failed.add(test_id) + continue try: output = example[self.testout_token][self.testout_token] @@ -98,44 +131,32 @@ class DocTests: try: input = example[self.testdat_token][self.testdat_token] 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: - 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') - + test_file_created = False if findex: scriptpath = os.path.dirname(os.path.realpath(__file__)) test_input_dir = scriptpath + '/../test/input/' - test_file = command[findex+1] - test_file_created = False + test_file = command[findex] if not os.path.exists(test_file): if input: test_file_created = True with open(test_file, 'w') as f: f.write(input) 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: verify = subprocess.check_output(command) except: verify = str() - if test_file_created: - os.remove(test_file) valid = (output == verify) + if valid and test_file_created: + os.remove(test_file) if self.verbose > 0: print test_id, ':', 'Passed' if valid else 'FAILED' else: -- cgit v1.2.3