From 15e5eced9623c42383eaeded1bc127de814b3b23 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Mon, 29 Apr 2019 23:06:25 +0300 Subject: Add: jt2 with binding, imlementation files --- Makefile | 2 +- jt2.bind | 72 +++++++++++++++++++++++++++++++ jt2.pdf | Bin 0 -> 103578 bytes jt2.py | 71 +++++++++++++++++++++++++++++++ jt2.yarn | 145 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 289 insertions(+), 1 deletion(-) create mode 100644 jt2.bind create mode 100644 jt2.pdf create mode 100644 jt2.py create mode 100644 jt2.yarn diff --git a/Makefile b/Makefile index 4589ec8..a7194c7 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -all: muck1.pdf muck2.pdf muck3.pdf jt1.pdf +all: muck1.pdf muck2.pdf muck3.pdf jt1.pdf jt2.pdf %.pdf: %.yarn pandoc -o $@ $< \ diff --git a/jt2.bind b/jt2.bind new file mode 100644 index 0000000..b7dfd0a --- /dev/null +++ b/jt2.bind @@ -0,0 +1,72 @@ +# Bind steps to functions that implement them. Each step is +# represented as a dict in a list with specific keys in the dict. +# +# given/when/then: specifies the keyword and a regexp that matches the +# step text, and captures parts of the step, using special syntax to +# name each match +# +# args: specifies type of each named match (argument), with type names +# being str (any text), int, pathname, ... +# +# function: name of function to be called, which will get the named +# arguments as parameters, in the order given in the regexp; function +# should return a value that indicates success or failure, and can +# contain values with more detail. Might be Rust enums or Python +# dict objects. +# +# capture_result: if set to true, a successful result is stored for +# inspection by a later step. It's a parse-time error is the step is +# not inspected by a later step in the scenario. Failure result still +# fails the step. +# +# use_captured_result: if set to true, the captured result is given to +# the function in a specific way, which depends on the language used +# to implement the function (e.g., keyword arg in Python). +# +# set_it: a specific argument captured from the step is remembered as +# "it". +# +# it: expects a value for "it" to have been rememered by a previous +# step, and the value must be of the specified type. If it isn't, +# that's a parsing-time error. + +- given: an empty journal repository + function: create_empty_git_repository + +- then: there is only (filename:\S+) in the journal repository + args: + filename: pathname + function: only_filename + +- when: I run (cmd:jt .+) + args: + cmd: {type: str, style: literal} + capture_result: true + function: run_jt + +- then: (filename:\S+) exists + args: + filename: pathname + set_it: filename + function: exists + +- then: it contains "(text:.+)" + args: + text: str + it: pathname + function: file_contains + +- then: it fails + use_captured_result: true + function: shell_command_failed + +- then: it fails with (error:.+) + args: + error: str + use_captured_result: true + function: stderr_contains + +- given: the date is (date:\d+-\d+-\d+) + args: + date: str + function: set_date diff --git a/jt2.pdf b/jt2.pdf new file mode 100644 index 0000000..46a3057 Binary files /dev/null and b/jt2.pdf differ diff --git a/jt2.py b/jt2.py new file mode 100644 index 0000000..7d5304e --- /dev/null +++ b/jt2.py @@ -0,0 +1,71 @@ +# This defines the functions that implement each step. Functions +# return either a saga.Success or a saga.Failure object. These are +# subclasses of dict, and the runner generated by Saga will abort if a +# step returns failure. Either dict may contain arbitrary fields. +# +# For simplicity, the runner will capture exceptions, and treats them +# as failures with the exception stored within it. Also, it treats a +# None return value as success with no keys. +# +# Values may be saved in named variables with the saga.set_variable +# function and retrieved with the saga.get_variable function. It's an +# error to retrieve an unset variable. + + +import os + +import saga + + +# This either returns None or raises an exception. +def create_empty_git_repository(): + subprocess.check-call(['git', 'init', '.'], stderr=subprocess.STDOUT) + + +# This either returns None or raises an exception. +def only_filename(filename): + assert os.listdir('.') == [filename] + + +# This run jt, with the --date= option that tells jt to pretend the +# current date is as given. The runner sets PATH to have the source +# directory of the project being tested. +def run_jt(cmd): + prefix = 'jt ' + assert cmd.startswith(prefix) + args = cmd[len(prefix:)] + + date = saga.get_variable('date') + shellcmd = "jt --date={} {}".format(date, args) + + exitcode, output = subprocess.getstatusoutput( + ['sh', '-c', shellcmd], stderr=subprocess.STDOUT) + + # We always return success, and expect the result to be checked by + # a later step. + return saga.Success(exitcode=exitcode, output=output) + + +def exists(filename): + assert os.path.exists(filename) + + +def file_contains(filename, text): + content = open(filename).read() + assert text in content + + +def shell_command_failed(captured_result=None): + exitcode = captured['exitcode'] + assert exitcode == 0 + + +def stderr_contains(error, captured_result=None): + stderr = captured_result['stderr'] + assert error in stderr + + +# These need to be passed to jt somehow, not sure how. That's not +# important for now. +def set_date(date): + saga.save_variable('date', date) diff --git a/jt2.yarn b/jt2.yarn new file mode 100644 index 0000000..e390893 --- /dev/null +++ b/jt2.yarn @@ -0,0 +1,145 @@ +--- +title: JT acceptance tests v2 +author: Lars Wirzenius / The Ick project +... + + +# Introduction + +JT is a command line tool for writing entries in my personal journal. +It works by adding files to the git repository with the journal, for +formatting by the Ikiwiki website compiler. This +document presents its automated acceptance tests, using a (for now +hypothetical) language similar to the Gherkin langauge implemented by +Cucumber. + +All of these scenarios start with an empty directory and with JT +configured to put drafts in the `drafts` directory, topics in +`topics`, and finished journal entries in `notes`. + +# Happy path scenarios + +## Add a journal entry + +We start off with an empty journal repository. We only test that it's +empty here; all other scenarios assume this works. + +```saga +given an empty journal repository +then there is only .git in the journal repository +``` + +We create a draft journal entry. + +```saga +when I run jt new "my title" +then drafts/0.mdwn exists +and it contains "my title" +``` + +When we finish the entry, the draft is moved to the notes directory. + +```saga +given the date is 2019-09-01 +when I run jt finish +then notes/2019/09/01/my_title.mdwn exists +and it contains "my title" +``` + +## Add a journal entry with an attachment + +This is similar to adding a journal entry, except it adds an +attachment to the new entry. The attachment could be anything, such as +a photo. + +```saga +given an empty journal repository +when I run jt new "my title" +then drafts/0.mdwn exists +and it contains "my title" +``` + +The attachment file needs to exist. It gets copied to the drafts +directory. + +```saga +given the file photo.jpg exists +when I run jt attach 0 photo.jpg +then drafts/0/photo.jpg exists +``` + +When we finish the entry, the draft is moved to the notes directory, +with the attachment. + +```saga +given the date is 2019-09-01 +when I run jt finish +then notes/2019/09/01/my_title.mdwn exists +and it contains "my title" +and notes/2019/09/01/my_title/photo.jpg exists +``` + + +## Add a topic + +JT allows "topic", which are meant to be pages that collect entries of +specific topics. The formatted journal will have a page for each topic +shows all entries that reference that topic. Similar to tags, but +another dimension of metadata. It's up to the user to use tags or +topics as they wish. + +```saga +given an empty journal repository +when I run jt new-topic 2019/my-project +then topics/2019/my-project.mdwn exists +``` + +One can add an entry for a topic, but it must exist. + +```saga +when I run jt new --topic nonexistent "my title" +then it fails +when I run jt new --topic 2019/my-project "my title" +then drafts/0.mdwn exists +and it contains "2019/my-project" +``` + +# Unhappy path scenarios + +## Finishing when there's more than one draft + +JT will pick the draft to act on if there's only one current draft. If +there's more, it will fail with an error message saying "too many +drafts". + +```saga +given an empty journal repository +when I run jt new "first" +then drafts/0.mdwn exists +when I run `jt new "second" +then **drafts/1.mdwn_** exists +when I run `jt finish` +then it fails with "too many drafts" +``` + +## Attaching when there's no such draft + +Attaching to a draft that doesn't exist doesn't work. + +```saga +> given an empty journal repository +> and the file photo.jpg exists +> when I run jt attach 0 photo.jpg +> then it fails with "no such draft" +``` + +## Attaching when there's no such file + +Attaching a file that doesn't exist doesn't work. + +```saga +given an empty journal repository +when I run jt new "first" +and I run jt attach 0 photo.jpg +then it fails with "no such file" +``` -- cgit v1.2.1