diff options
-rw-r--r-- | fable.py | 90 | ||||
-rwxr-xr-x | ftt-codegen | 11 | ||||
-rw-r--r-- | jt-prelude.py | 1 | ||||
-rw-r--r-- | jt.md | 4 | ||||
-rw-r--r-- | jt.py | 145 | ||||
-rw-r--r-- | jt.yaml | 55 |
6 files changed, 288 insertions, 18 deletions
@@ -1,7 +1,10 @@ # Note that the API of this module is guaranteed to change. +import argparse +import logging import os import sys +import shutil import tempfile @@ -20,30 +23,111 @@ class Context: self._vars[key] = value +class Settingses: + + def __init__(self): + self._scenarios = [] + + def add_scenario(self, name): + self._scenarios.append(name) + + def is_wanted_scenario(self, name): + return (not self._scenarios or + any(name.casefold() == w.casefold() for w in self._scenarios)) + + +class Datadir: + + def __init__(self): + self._dirname = None + self._ours = False + + def get(self): + if self._dirname is None: + self._dirname = tempfile.mkdtemp() + logging.info('Created temporary directory %s', self._dirname) + self._ours = True + return self._dirname + + def set(self, dirname): + self._dirname = os.path.abspath(dirname) + if not os.path.exists(self._dirname): + os.mkdir(self._dirname) + self._ours = False + + def cleanup(self): + if self._ours: + logging.debug('Removing %s', self._dirname) + shutil.rmtree(self._dirname) + + class Run: def __init__(self): self._name = None self._context = None self._srcdir = None - self._tmpdir = None + self._settingses = Settingses() + self._datadir = None + self._tempdir = None + self._parse_options() + + def _parse_options(self): + p = argparse.ArgumentParser() + p.add_argument('--log') + p.add_argument('--tempdir') + p.add_argument( + '--scenario', '-s', action='append', dest='scenarios', default=[]) + args = p.parse_args() + self._setup_logging(args.log) + self._tempdir = args.tempdir + for s in args.scenarios: + self._settingses.add_scenario(s) + + def _setup_logging(self, filename): + format = '%(asctime)s %(levelname)s %(message)s' + logging.basicConfig( + filename=filename, format=format, level=logging.DEBUG) + logging.info('test program starts =============') def get_context(self): return self._context def start(self, name): + if not self._settingses.is_wanted_scenario(name): + logging.info('Skipping unwanted scenario %s', name) + return False + + logging.info('Start scenario %s', name) self._context = Context() self._name = name - self._tmpdir = tempfile.mkdtemp() self._srcdir = os.getcwd() - os.chdir(self._tmpdir) + self._datadir = Datadir() + self._datadir.set(self._tempdir) + dirname = tempfile.mkdtemp(dir=self._datadir.get()) + os.chdir(dirname) + return True def end(self): + logging.info('OK %s', self._name) sys.stdout.write('OK: {}\n'.format(self._name)) os.chdir(self._srcdir) + self._datadir.cleanup() + + +def assertTrue(a): + if not a: + raise Exception( + 'expected {!r} to be true, but was disappointed'.format(a, b)) def assertEqual(a, b): if a != b: raise Exception( 'expected {!r} == {!r}, but was disappointed'.format(a, b)) + + +def assertNotEqual(a, b): + if a == b: + raise Exception( + 'expected {!r} != {!r}, but was disappointed'.format(a, b)) diff --git a/ftt-codegen b/ftt-codegen index c54a383..694cc05 100755 --- a/ftt-codegen +++ b/ftt-codegen @@ -118,8 +118,10 @@ def codegen(f, step, bindings): keyword = words[0] rest = ' '.join(words[1:]) function, args = find_binding(bindings, keyword, rest) - f.write('args = {}\n'.format(json.dumps(args))) - f.write('{}(run.get_context(), **args)\n'.format(function)) + f.write(' logging.info("step %s", {})\n'.format(json.dumps(step))) + f.write(' args = {}\n'.format(json.dumps(args))) + f.write(' logging.debug("calling {} with args %s", args)\n'.format(function)) + f.write(' {}(run.get_context(), **args)\n\n'.format(function)) def debug(msg): if False: @@ -134,6 +136,7 @@ debug('reading prelude') prelude = open(sys.argv[2]).read() sys.stdout.write(hardcoded) sys.stdout.write(prelude) +sys.stdout.write('\n') debug('reading inputs') text = ''.join(open(filename).read() for filename in sys.argv[3:]) @@ -154,9 +157,9 @@ for s in fable.get_scenarios(): f = sys.stdout for s in scenarios: name = s.get_name() - f.write('run.start("{}")\n'.format(name)) + f.write('\nif run.start("{}"):\n'.format(name)) for step in s.get_steps(): codegen(f, step, bindings) - f.write('run.end()\n') + f.write(' run.end()\n') debug('ok') diff --git a/jt-prelude.py b/jt-prelude.py new file mode 100644 index 0000000..be5c08f --- /dev/null +++ b/jt-prelude.py @@ -0,0 +1 @@ +from jt import * @@ -109,7 +109,7 @@ and I run jt new "second entry" then there are 2 drafts and draft 0 has title "first entry" and draft 1 has title "second entry" -when I run jt finish +when I try to run jt finish then it fails ``` @@ -135,7 +135,7 @@ always requires specifying the draft. given an empty journal when I run jt new "random title" then there is 1 draft -when I run jt remove +when I try to run jt remove then it fails when I run jt remove 0 then there are no drafts @@ -0,0 +1,145 @@ +import logging +import os +import subprocess +import sys + +import fable + +def _jt_path(): + return os.environ['JT_PATH'] + +def _runcmd(ctx, argv): + logging.debug('Executing %r', argv) + p = subprocess.Popen( + argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + out, err = p.communicate() + ctx["stdout"] = out + ctx["stderr"] = err + ctx["exit_code"] = p.returncode + logging.debug('Result: exit_code=%r', ctx['exit_code']) + if ctx['exit_code'] != 0: + logging.warning( + 'jt failed, stdout=%r stderr=%r', ctx['stdout'], ctx['stderr']) + +def create_empty_journal(ctx): + ctx['journal'] = os.path.abspath('journal') + ctx['blog'] = os.path.join(ctx['journal'], 'blog') + os.mkdir(ctx['journal']) + + ctx['config'] = os.path.abspath('jt.conf') + with open(ctx['config'], 'w') as f: + f.write('[config]\n') + f.write('source = %s\n' % ctx['journal']) + f.write('git = no\n') + f.write('push = no\n') + f.write('notes-dir = blog\n') + f.write('editor = /bin/true {editor}\n') + f.write('log = %s.log\n' % ctx['journal']) + logging.debug('Created config file %s', ctx['config']) + + +def run_jt(ctx, command=None): + try_jt(ctx, command=command) + fable.assertEqual(ctx['exit_code'], 0) + +def try_jt(ctx, command=None): + args = command.split() + assert args[0] == 'jt' + + opts = [ + '--no-default-config', + '--config', ctx['config'], + ] + + argv = [_jt_path()] + opts + args[1:] + argv = ['/bin/sh', '-c', ' '.join(argv)] + _runcmd(ctx, argv) + +def create_empty_file(ctx, filename=None): + with open(filename, 'w') as f: + pass + +def jt_failed(ctx): + fable.assertNotEqual(ctx['exit_code'], 0) + +def _find_drafts(ctx): + draftsdir = os.path.join(ctx['journal'], 'drafts') + if not os.path.isdir(draftsdir): + return [] + drafts = [f for f in _find_files(draftsdir) if f.endswith('.mdwn')] + logging.debug('drafts = %r', drafts) + return drafts + +def no_drafts(ctx): + drafts = _find_drafts(ctx) + fable.assertEqual(drafts, []) + +def one_draft(ctx): + drafts = _find_drafts(ctx) + fable.assertEqual(len(drafts), 1) + +def one_draft_with_attachment(ctx, filename=None): + drafts = _find_drafts(ctx) + fable.assertEqual(len(drafts), 1) + filename = drafts[0] + fable.assertTrue(filename.endswith('.mdwn')) + dirname = filename[:-len('.mdwn')] + fable.assertTrue(os.path.isdir(dirname)) + attachment = os.path.join(dirname, filename) + fable.assertTrue(os.path.exists(attachment)) + +def n_drafts(ctx, count=None): + n = int(count) + drafts = _find_drafts(ctx) + fable.assertEqual(len(drafts), n) + +def _get_draft(ctx, id): + filename = os.path.join(ctx['journal'], 'drafts', '%s.mdwn' % id) + return _cat(filename) + +def draft_has_title(ctx, id=None, title=None): + draft = _get_draft(ctx, id) + fable.assertTrue(title in draft) + +def _find_files(dirname): + for d, s, f in os.walk(dirname): + for filename in f: + yield os.path.join(d, filename) + +def _find_entries(ctx): + blogdir = ctx['blog'] + logging.debug('blogdir: %r', blogdir) + if not os.path.isdir(blogdir): + return [] + entries = [f for f in _find_files(blogdir) if f.endswith('.mdwn')] + logging.debug('entries = %r', entries) + return entries + +def one_entry(ctx): + entries = _find_entries(ctx) + fable.assertEqual(len(entries), 1) + +def n_entries(ctx, count=None): + n = int(count) + entries = _find_entries(ctx) + fable.assertEqual(len(entries), n) + +def no_entries(ctx): + entries = _find_entries(ctx) + fable.assertEqual(len(entries), 0) + +def _find_topics(ctx): + dirname = os.path.join(ctx['journal'], 'topics') + filenames = [f for f in _find_files(dirname) if f.endswith('.mdwn')] + logging.debug('topics: %r', filenames) + return filenames + +def _cat(filename): + with open(filename) as f: + return f.read() + +def one_topic(ctx, topic=None, title=None): + topics = _find_topics(ctx) + fable.assertEqual(len(topics), 1) + topic = _cat(topics[0]) + fable.assertTrue(title in topic) @@ -1,10 +1,47 @@ -- given: a file (?P<filename>.+) -- when: I run (?P<cmd>jt .*) -- then: there is (?P<count>\d+) draft -- then: there is (?P<count>\d+) draft, with attachment (?P<filename>.+) -- then: there is (?P<count>\d+) journal entry -- then: there is (?P<count>\d+) journal entry, with attachment (?P<filename>.+) -- then: there is (?P<count>\d+) journal entry, belonging to (?P<id>.+) -- then: there is (?P<count>\d+) topic, (?P<id>.+), titled "(?P<title>.+)" +- given: a file (?P<filename>\S+) + function: create_empty_file + +- given: an empty journal + function: create_empty_journal + +- then: draft (?P<id>\d+) has title "(?P<title>.+)" + function: draft_has_title + +- then: it fails + function: jt_failed + +- then: there are (?P<count>\d+) drafts + function: n_drafts + - then: there are (?P<count>\d+) journal entries -- then: draft (?P<id>\d+) has title "(?P<title>.*)" + function: n_entries + +- then: there are no drafts + function: no_drafts + +- then: there are no journal entries + function: no_entries + +- then: there is 1 draft + function: one_draft + +- then: there is 1 draft, with attachment (?P<filename>\S+) + function: one_draft_with_attachment + +- then: there is 1 journal entry + function: one_entry + +- then: there is 1 journal entry, belonging to (?P<topic>\S+) + function: one_entry_on_topic + +- then: there is 1 journal entry, with attachment (?P<filename>\S+) + function: one_enry_with_attachment + +- then: there there is 1 topic, (?P<topic>\S+), titled "(?P<title>.+)" + function: one_topic + +- when: I run (?P<command>jt .*) + function: run_jt + +- when: I try to run (?P<command>jt .*) + function: try_jt |