diff options
-rw-r--r-- | .gitignore | 5 | ||||
-rw-r--r-- | Cargo.lock | 251 | ||||
-rw-r--r-- | Cargo.toml | 10 | ||||
-rwxr-xr-x | check | 63 | ||||
-rw-r--r-- | jt.md | 33 | ||||
-rw-r--r-- | jt.py | 28 | ||||
-rw-r--r-- | jt.yaml | 16 | ||||
-rw-r--r-- | runcmd.py | 77 | ||||
-rw-r--r-- | runcmd.yaml | 13 | ||||
-rw-r--r-- | src/lib.rs | 1 | ||||
-rw-r--r-- | src/main.rs | 54 |
11 files changed, 550 insertions, 1 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3b10284 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/target +*.pdf +*.html +test.py +test.log diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..8f97a58 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,251 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] +name = "anyhow" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b602bfe940d21c130f3895acd65221e8a61270debe89d628b9cb4e3ccb8569b" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9" +dependencies = [ + "libc", +] + +[[package]] +name = "jt2" +version = "0.1.0" +dependencies = [ + "anyhow", + "structopt", + "thiserror", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "755456fae044e6fa1ebbbd1b3e902ae19e73097ed4ed87bb79934a867c007bc3" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175c513d55719db99da20232b06cda8bab6b83ec2d04e3283edf0213c37c1a29" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "structopt" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cc388d94ffabf39b5ed5fadddc40147cb21e605f53db6f8f36a625d27489ac5" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2513111825077552a6751dfad9e11ce0fba07d7276a3943a037d7e93e64c5f" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d8d6567fe7c7f8835a3a98af4208f3846fba258c1bc3c31d6e506239f11f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-segmentation" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2066c46 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "jt2" +version = "0.1.0" +authors = ["Lars Wirzenius <liw@liw.fi>"] +edition = "2018" + +[dependencies] +structopt = "0.3" +anyhow = "1" +thiserror = "1" @@ -0,0 +1,63 @@ +#!/bin/sh + +set -eu + +verbose=false +if [ "$#" -gt 0 ] +then + case "$1" in + verbose | -v | --verbose) + verbose=true + ;; + esac +fi + +hideok= + +if command -v chronic > /dev/null +then + hideok=chronic +fi + +if $verbose +then + hideok= +fi + + +codegen() { + $hideok sp-codegen "$1" --output "$2" --run +} + +docgen() { + $hideok sp-docgen "$1" --output "$2" +} + +$hideok cargo build --all-targets +if cargo --list | awk '{ print $1 }' | grep 'clippy$' > /dev/null +then + $hideok cargo clippy +fi +$hideok cargo test + +if command -v rustfmt > /dev/null +then + find src -type f -name '*.rs' -exec rustfmt --check '{}' + +fi + +if command -v black > /dev/null +then + $hideok find . -type f -name '*.py' ! -name template.py ! -name test.py \ + -exec black --check '{}' + +fi + +for md in [^CR]*.md +do + $hideok echo "$md =====================================" + codegen "$md" test.py + docgen "$md" "$(basename "$md" .md).pdf" + docgen "$md" "$(basename "$md" .md).html" + $hideok echo +done + +echo "Everything seems to be in order." @@ -23,17 +23,48 @@ journal for the user, and a new journal entry. The entry is a draft until it's finished. ~~~sh -$ jt create ~/Journal default "My private journal" +$ jt init ~/Journal default "My private journal" $ jt new --tag meta --tag journalling "My first journal entry" ... # text editor is opened so the new entry can be written $ jt finish ~~~ +# Acceptance criteria and their verification + +This chapter defines detailed acceptance criteria and how they're +verified using *scenarios* for the [Subplot][] tool. + +[Subplot]: https://subplot.liw.fi/ + +## Create a new local journal repository + +`jt` works on a local repository, and it can be created an initialised +using the tool. + +~~~scenario +when I run jt init jrnl default "My test journal" +then program finished successfully +and directory jrnl exists + +when I run jt is-journal jrnl +then program finished successfully + +when I run jt is-journal bogus +then exit code is non-zero +~~~ + + --- title: "jt—a journalling tool" author: - Lars Wirzenius - Daniel Silverstone +bindings: +- jt.yaml +- runcmd.yaml +functions: +- jt.py +- runcmd.py ... @@ -0,0 +1,28 @@ +import logging +import os + + +def run_jt_init(ctx, dirname=None, journalname=None, title=None): + runcmd(ctx, [_binary("jt"), "init", dirname, journalname, title]) + + +def run_jt_list_journals(ctx): + runcmd(ctx, [_binary("jt"), "list-journals"]) + + +def run_jt_is_journal(ctx, dirname=None): + runcmd(ctx, [_binary("jt"), "is-journal", dirname]) + + +def _binary(name): + return os.path.join(srcdir, "target", "debug", "jt2") + + +def is_directory(ctx, dirname=None): + logging.debug("checking if %r is a directory", dirname) + assert_eq(os.path.isdir(dirname), True) + + +def output_contains(ctx, pattern=None): + logging.debug("checking if %r contains", ctx["stdout"], pattern) + assert_eq(pattern in ctx["stdout"], True) @@ -0,0 +1,16 @@ +- when: I run jt init (?P<dirname>\S+) (?P<journalname>\S+) "(?P<title>.*)" + regex: true + function: run_jt_init + +- when: I run jt list-journals + function: run_jt_list_journals + +- when: I run jt is-journal {dirname} + function: run_jt_is_journal + +- then: directory {dirname} exists + function: is_directory + +- then: output contains "(?P<pattern>.*)" + regex: true + function: output_contains diff --git a/runcmd.py b/runcmd.py new file mode 100644 index 0000000..7193c15 --- /dev/null +++ b/runcmd.py @@ -0,0 +1,77 @@ +# Some step implementations for running commands and capturing the result. + +import subprocess + + +# Run a command, capture its stdout, stderr, and exit code in context. +def runcmd(ctx, argv, **kwargs): + p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs) + stdout, stderr = p.communicate("") + ctx["argv"] = argv + ctx["stdout"] = stdout.decode("utf-8") + ctx["stderr"] = stderr.decode("utf-8") + ctx["exit"] = p.returncode + + +# Check that latest exit code captured by runcmd was a specific one. +def exit_code_is(ctx, wanted): + if ctx.get("exit") != wanted: + print("context:", ctx.as_dict()) + assert_eq(ctx.get("exit"), wanted) + + +# Check that latest exit code captured by runcmd was not a specific one. +def exit_code_is_not(ctx, unwanted): + if ctx.get("exit") == unwanted: + print("context:", ctx.as_dict()) + assert_ne(ctx.get("exit"), unwanted) + + +# Check that latest exit code captured by runcmd was zero. +def exit_code_zero(ctx): + exit_code_is(ctx, 0) + + +# Check that latest exit code captured by runcmd was not zero. +def exit_code_nonzero(ctx): + exit_code_is_not(ctx, 0) + + +# Check that stdout of latest runcmd contains a specific string. +def stdout_contains(ctx, pattern=None): + stdout = ctx.get("stdout", "") + if pattern not in stdout: + print("pattern:", repr(pattern)) + print("stdout:", repr(stdout)) + print("ctx:", ctx.as_dict()) + assert_eq(pattern in stdout, True) + + +# Check that stdout of latest runcmd does not contain a specific string. +def stdout_does_not_contain(ctx, pattern=None): + stdout = ctx.get("stdout", "") + if pattern in stdout: + print("pattern:", repr(pattern)) + print("stdout:", repr(stdout)) + print("ctx:", ctx.as_dict()) + assert_eq(pattern not in stdout, True) + + +# Check that stderr of latest runcmd does contains a specific string. +def stderr_contains(ctx, pattern=None): + stderr = ctx.get("stderr", "") + if pattern not in stderr: + print("pattern:", repr(pattern)) + print("stderr:", repr(stderr)) + print("ctx:", ctx.as_dict()) + assert_eq(pattern in stderr, True) + + +# Check that stderr of latest runcmd does not contain a specific string. +def stderr_does_not_contain(ctx, pattern=None): + stderr = ctx.get("stderr", "") + if pattern not in stderr: + print("pattern:", repr(pattern)) + print("stderr:", repr(stderr)) + print("ctx:", ctx.as_dict()) + assert_eq(pattern not in stderr, True) diff --git a/runcmd.yaml b/runcmd.yaml new file mode 100644 index 0000000..02e5ee1 --- /dev/null +++ b/runcmd.yaml @@ -0,0 +1,13 @@ +- then: exit code is non-zero + function: exit_code_nonzero + +- then: output matches /(?P<pattern>.+)/ + function: stdout_contains + regex: true + +- then: stderr matches /(?P<pattern>.+)/ + function: stderr_contains + regex: true + +- then: program finished successfully + function: exit_code_zero diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1 @@ + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0a7efe1 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,54 @@ +use anyhow::Result; +use std::fs; +use std::path::{Path, PathBuf}; +use structopt::StructOpt; +use thiserror::Error; + +#[derive(Debug, StructOpt)] +#[structopt(about = "maintain a journal")] +enum JT { + Init { + #[structopt(help = "Directory where journal should be stored")] + dirname: PathBuf, + #[structopt(help = "Short name for journal")] + journalname: String, + #[structopt(help = "Short description of journal, its title")] + description: String, + }, + IsJournal { + #[structopt(help = "Directory that may or may not be a journal")] + dirname: PathBuf, + }, +} + +#[derive(Debug, Error)] +enum JournalError { + #[error("directory {0} is not a journal")] + NotAJournal(String), +} + +fn main() -> Result<()> { + let opt = JT::from_args(); + match opt { + JT::Init { + dirname, + journalname, + description, + } => init(&dirname, &journalname, &description)?, + JT::IsJournal { dirname } => is_journal(&dirname)?, + } + Ok(()) +} + +fn init(dirname: &Path, _journalname: &str, _description: &str) -> anyhow::Result<()> { + std::fs::create_dir(dirname)?; + Ok(()) +} + +fn is_journal(dirname: &Path) -> anyhow::Result<()> { + let meta = fs::symlink_metadata(dirname)?; + if !meta.is_dir() { + return Err(JournalError::NotAJournal(dirname.to_string_lossy().to_string()).into()); + } + Ok(()) +} |