summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2021-01-30 09:59:20 +0000
committerLars Wirzenius <liw@liw.fi>2021-01-30 09:59:20 +0000
commitd54b4a40e8151e0bac395d94222085f264da25f8 (patch)
tree285939851c69cdafb9368ca23ff7c5497f069630
parent84527e4bc62e69800cedbcca85452173173c4d29 (diff)
parent857e86ccd32a427cce5dd9df6402fd1d458c5c7a (diff)
downloadewww-d54b4a40e8151e0bac395d94222085f264da25f8.tar.gz
Merge branch 'debian' into 'main'
Update stuff, fix stuff, add Debian packaging See merge request larswirzenius/ewww!14
-rw-r--r--debian/cargo-checksum.json0
-rw-r--r--debian/changelog6
-rw-r--r--debian/compat2
-rw-r--r--debian/control26
-rw-r--r--debian/copyright23
-rwxr-xr-xdebian/rules15
-rw-r--r--debian/source/format1
-rw-r--r--ewww.md16
-rw-r--r--subplot/ewww.py42
-rw-r--r--subplot/ewww.yaml8
-rw-r--r--subplot/vendored/daemon.py256
-rw-r--r--subplot/vendored/daemon.yaml37
-rw-r--r--subplot/vendored/files.py158
-rw-r--r--subplot/vendored/files.yaml62
-rw-r--r--subplot/vendored/runcmd.py (renamed from subplot/runcmd.py)9
-rw-r--r--subplot/vendored/runcmd.yaml (renamed from subplot/runcmd.yaml)0
16 files changed, 621 insertions, 40 deletions
diff --git a/debian/cargo-checksum.json b/debian/cargo-checksum.json
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/debian/cargo-checksum.json
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..fa45612
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,6 @@
+ewww (0.1-1) unstable; urgency=low
+
+ * Initial packaging. This is not intended to be uploaded to Debian, so
+ no closing of an ITP bug.
+
+ -- Lars Wirzenius <liw@liw.fi> Sat, 28 Sep 2019 16:45:49 +0300
diff --git a/debian/compat b/debian/compat
new file mode 100644
index 0000000..021ea30
--- /dev/null
+++ b/debian/compat
@@ -0,0 +1,2 @@
+10
+
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..91b0975
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,26 @@
+Source: ewww
+Maintainer: Lars Wirzenius <liw@liw.fi>
+Section: www
+Priority: optional
+Standards-Version: 4.2.0
+Build-Depends:
+ debhelper (>= 10~),
+ build-essential,
+ dh-cargo,
+ daemonize,
+ git,
+ pkg-config,
+ python3,
+ python3-requests,
+ subplot,
+ texlive-fonts-recommended,
+ texlive-latex-base,
+ texlive-latex-recommended
+Homepage: https://obnam.org
+
+Package: ewww
+Architecture: any
+Depends: ${misc:Depends}, ${shlibs:Depends}
+Built-Using: ${cargo:Built-Using}
+Description: simplistic web server for static content
+ Ewww serves static web content.
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..89db566
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,23 @@
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: ewww
+Upstream-Contact: Lars Wirzenius <liw@liw.fi>
+Source: http://git.liw.fi/ewww
+
+Files: *
+Copyright: 2020, Lars Wirzenius
+License: GPL-3+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+ .
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+ .
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ .
+ On a Debian system, you can find a copy of GPL version 3 at
+ /usr/share/common-licenses/GPL-3 .
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..9385a1b
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,15 @@
+#!/usr/bin/make -f
+
+%:
+ dh $@ --buildsystem cargo
+
+override_dh_auto_build:
+ true
+
+override_dh_auto_install:
+ cargo install --path=. --root=debian/ewww
+ rm -f debian/ewww/.crates.toml
+ rm -f debian/ewww/.crates2.json
+
+override_dh_auto_test:
+ ./check
diff --git a/debian/source/format b/debian/source/format
new file mode 100644
index 0000000..163aaf8
--- /dev/null
+++ b/debian/source/format
@@ -0,0 +1 @@
+3.0 (quilt)
diff --git a/ewww.md b/ewww.md
index 1c5780f..d1c9faf 100644
--- a/ewww.md
+++ b/ewww.md
@@ -92,7 +92,8 @@ for static content only. Every other method returns an error.
~~~scenario
given a self-signed certificate as snakeoil.pem, using key snakeoil.key
-and file webroot/foo.html with "this is your web page"
+and directory webroot
+and file webroot/foo.html from webpage.html
and a running server using config file smoke.yaml
when I request GET https://example.com/foo.html
then I get status code 200
@@ -110,6 +111,10 @@ tls_cert: snakeoil.pem
tls_key: snakeoil.key
~~~
+~~~{#webpage.html .file add-newline=no}
+this is your web page
+~~~
+
## Performance test
~~~scenario-disabled
@@ -146,12 +151,15 @@ author: Lars Wirzenius
template: python
bindings:
- subplot/ewww.yaml
- - subplot/runcmd.yaml
+ - subplot/vendored/daemon.yaml
+ - subplot/vendored/files.yaml
+ - subplot/vendored/runcmd.yaml
functions:
- subplot/ewww.py
- - subplot/daemon.py
- subplot/http.py
- - subplot/runcmd.py
+ - subplot/vendored/daemon.py
+ - subplot/vendored/files.py
+ - subplot/vendored/runcmd.py
classes:
- scenario-disabled
...
diff --git a/subplot/ewww.py b/subplot/ewww.py
index 43e5946..f1e02e3 100644
--- a/subplot/ewww.py
+++ b/subplot/ewww.py
@@ -5,8 +5,6 @@ import logging
import os
import random
import shutil
-import socket
-import time
import urllib.parse
import yaml
@@ -42,12 +40,11 @@ def fixme(*args, **kwargs):
assert 0
-# Create a file.
-def create_file(ctx, filename=None, content=None):
- logging.debug(f"Creating file {filename} with {content}")
- dirname = os.path.dirname(filename)
- os.makedirs(dirname)
- _write(filename, content)
+# Create a directory.
+def create_directory(ctx, dirname=None):
+ dirname = "./" + dirname
+ if not os.path.exists(dirname):
+ os.makedirs(dirname)
# Copy test certificate from source tree, where it's been created previously by
@@ -62,7 +59,8 @@ def copy_test_certificate(ctx, cert=None, key=None):
# Start server using named configuration file.
def start_server(ctx, filename=None):
get_file = globals()["get_file"]
- start_daemon = globals()["start_daemon"]
+ daemon_start_on_port = globals()["daemon_start_on_port"]
+
logging.debug(f"Starting ewww with config file {filename}")
config = get_file(filename).decode("UTF-8")
config = yaml.safe_load(config)
@@ -72,32 +70,16 @@ def start_server(ctx, filename=None):
config = yaml.safe_dump(config)
_write(filename, config)
- start_daemon(ctx, "ewww", [_binary("ewww"), filename])
-
- if not port_open("localhost", port, 5.0):
- stderr = open(ctx["daemon"]["ewww"]["stderr"]).read()
- logging.debug(f"Stderr from daemon: {stderr!r}")
-
-
-# Wait for a port to be open
-def port_open(host, port, timeout):
- logging.debug(f"Waiting for port localhost:{port} to be available")
- started = time.time()
- while time.time() < started + timeout:
- try:
- socket.create_connection((host, port), timeout=timeout)
- return True
- except socket.error:
- pass
- logging.error(f"Port localhost:{port} is not open")
- return False
+ daemon_start_on_port(
+ ctx, path=_binary("ewww"), args=filename, name="ewww", port=port
+ )
# Stop previously started server.
def stop_server(ctx):
- stop_daemon = globals()["stop_daemon"]
+ daemon_stop = globals()["daemon_stop"]
logging.debug("Stopping ewww")
- stop_daemon(ctx, "ewww")
+ daemon_stop(ctx, name="ewww")
# Make an HTTP request.
diff --git a/subplot/ewww.yaml b/subplot/ewww.yaml
index 375558d..7353863 100644
--- a/subplot/ewww.yaml
+++ b/subplot/ewww.yaml
@@ -5,12 +5,8 @@
function: start_server
cleanup: stop_server
-- given: "{count} files in {dirname}"
- function: fixme
-
-- given: file (?P<filename>\S+) with "(?P<content>.*)"
- regex: true
- function: create_file
+- given: directory {dirname}
+ function: create_directory
- then: I am redirected to {location}
function: fixme
diff --git a/subplot/vendored/daemon.py b/subplot/vendored/daemon.py
new file mode 100644
index 0000000..d436b4f
--- /dev/null
+++ b/subplot/vendored/daemon.py
@@ -0,0 +1,256 @@
+import logging
+import os
+import signal
+import socket
+import subprocess
+import time
+
+
+# A helper function for testing lib/daemon itself.
+def _daemon_shell_script(ctx, filename=None):
+ get_file = globals()["get_file"]
+ data = get_file(filename)
+ with open(filename, "wb") as f:
+ f.write(data)
+ os.chmod(filename, 0o755)
+
+
+# Start a daemon that will open a port on localhost.
+def daemon_start_on_port(ctx, path=None, args=None, name=None, port=None):
+ _daemon_start(ctx, path=path, args=args, name=name)
+ daemon_wait_for_port("localhost", port)
+
+
+# Start a daemon after a little wait. This is used only for testing the
+# port-waiting code.
+def _daemon_start_soonish(ctx, path=None, args=None, name=None, port=None):
+ _daemon_start(ctx, path=os.path.abspath(path), args=args, name=name)
+ daemon = ctx.declare("_daemon")
+
+ # Store the PID of the process we just started so that _daemon_stop_soonish
+ # can kill it during the cleanup phase. This works around the Subplot
+ # Python template not giving the step captures to cleanup functions. Note
+ # that this code assume at most one _soonish function is called.
+ daemon["_soonish"] = daemon[name]["pid"]
+
+ try:
+ daemon_wait_for_port("localhost", port)
+ except Exception as e:
+ daemon["_start_error"] = repr(e)
+
+ logging.info("pgrep: %r", _daemon_pgrep(path))
+
+
+def _daemon_stop_soonish(ctx, path=None, args=None, name=None, port=None):
+ ns = ctx.declare("_daemon")
+ pid = ns["_soonish"]
+ logging.debug(f"Stopping soonishly-started daemon, {pid}")
+ signo = signal.SIGKILL
+ try:
+ os.kill(pid, signo)
+ except ProcessLookupError:
+ logging.warning("Process did not actually exist (anymore?)")
+
+
+# Start a daeamon, get its PID. Don't wait for a port or anything. This is
+# meant for background processes that don't have port. Useful for testing the
+# lib/daemon library of Subplot, but not much else.
+def _daemon_start(ctx, path=None, args=None, name=None):
+ runcmd_run = globals()["runcmd_run"]
+ runcmd_exit_code_is = globals()["runcmd_exit_code_is"]
+ runcmd_get_exit_code = globals()["runcmd_get_exit_code"]
+ runcmd_get_stderr = globals()["runcmd_get_stderr"]
+ runcmd_prepend_to_path = globals()["runcmd_prepend_to_path"]
+
+ path = os.path.abspath(path)
+ argv = [path] + args.split()
+
+ logging.debug(f"Starting daemon {name}")
+ logging.debug(f" ctx={ctx.as_dict()}")
+ logging.debug(f" name={name}")
+ logging.debug(f" path={path}")
+ logging.debug(f" args={args}")
+ logging.debug(f" argv={argv}")
+
+ ns = ctx.declare("_daemon")
+
+ this = ns[name] = {
+ "pid-file": f"{name}.pid",
+ "stderr": f"{name}.stderr",
+ "stdout": f"{name}.stdout",
+ }
+
+ # Debian installs `daemonize` to /usr/sbin, which isn't part of the minimal
+ # environment that Subplot sets up. So we add /usr/sbin to the PATH.
+ runcmd_prepend_to_path(ctx, "/usr/sbin")
+ runcmd_run(
+ ctx,
+ [
+ "daemonize",
+ "-c",
+ os.getcwd(),
+ "-p",
+ this["pid-file"],
+ "-e",
+ this["stderr"],
+ "-o",
+ this["stdout"],
+ ]
+ + argv,
+ )
+
+ # Check that daemonize has exited OK. If it hasn't, it didn't start the
+ # background process at all. If so, log the stderr in case there was
+ # something useful there for debugging.
+ exit = runcmd_get_exit_code(ctx)
+ if exit != 0:
+ stderr = runcmd_get_stderr(ctx)
+ logging.error(f"daemon {name} stderr: {stderr}")
+ runcmd_exit_code_is(ctx, 0)
+
+ # Get the pid of the background process, from the pid file created by
+ # daemonize. We don't need to wait for it, since we know daemonize already
+ # exited. If it isn't there now, it's won't appear later.
+ if not os.path.exists(this["pid-file"]):
+ raise Exception("daemonize didn't create a PID file")
+
+ this["pid"] = _daemon_wait_for_pid(this["pid-file"], 10.0)
+
+ logging.debug(f"Started daemon {name}")
+ logging.debug(f" pid={this['pid']}")
+ logging.debug(f" ctx={ctx.as_dict()}")
+
+
+def _daemon_wait_for_pid(filename, timeout):
+ start = time.time()
+ while time.time() < start + timeout:
+ with open(filename) as f:
+ data = f.read().strip()
+ if data:
+ return int(data)
+ raise Exception("daemonize created a PID file without a PID")
+
+
+def daemon_wait_for_port(host, port, timeout=5.0):
+ addr = (host, port)
+ until = time.time() + timeout
+ while True:
+ try:
+ s = socket.create_connection(addr, timeout=timeout)
+ s.close()
+ return
+ except socket.timeout:
+ logging.error(f"daemon did not respond at port {port} within {timeout} seconds")
+ raise
+ except socket.error as e:
+ logging.info(f"could not connect to daemon at {port}: {e}")
+ pass
+ if time.time() >= until:
+ logging.error(f"could not connect to daemon at {port} within {timeout} seconds")
+ raise ConnectionRefusedError()
+ # Sleep a bit to avoid consuming too much CPU while busy-waiting.
+ time.sleep(0.1)
+
+
+# Stop a daemon.
+def daemon_stop(ctx, path=None, args=None, name=None):
+ logging.debug(f"Stopping daemon {name}")
+
+ ns = ctx.declare("_daemon")
+ logging.debug(f" ns={ns}")
+ pid = ns[name]["pid"]
+ signo = signal.SIGTERM
+
+ this = ns[name]
+ data = open(this["stdout"]).read()
+ logging.debug(f"{name} stdout, before: {data!r}")
+ data = open(this["stderr"]).read()
+ logging.debug(f"{name} stderr, before: {data!r}")
+
+ logging.debug(f"Terminating process {pid} with signal {signo}")
+ try:
+ os.kill(pid, signo)
+ except ProcessLookupError:
+ logging.warning("Process did not actually exist (anymore?)")
+
+ while True:
+ try:
+ os.kill(pid, 0)
+ logging.debug(f"Daemon {name}, pid {pid} still exists")
+ time.sleep(1)
+ except ProcessLookupError:
+ break
+ logging.debug(f"Daemon {name} is gone")
+
+ data = open(this["stdout"]).read()
+ logging.debug(f"{name} stdout, after: {data!r}")
+ data = open(this["stderr"]).read()
+ logging.debug(f"{name} stderr, after: {data!r}")
+
+
+def daemon_no_such_process(ctx, args=None):
+ assert not _daemon_pgrep(args)
+
+
+def daemon_process_exists(ctx, args=None):
+ assert _daemon_pgrep(args)
+
+
+def _daemon_pgrep(pattern):
+ logging.info(f"checking if process exists: pattern={pattern}")
+ exit = subprocess.call(["pgrep", "-laf", pattern], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ logging.info(f"exit code: {exit}")
+ return exit == 0
+
+
+def daemon_start_fails_with(ctx, message=None):
+ daemon = ctx.declare("_daemon")
+ error = daemon["_start_error"]
+ logging.debug(f"daemon_start_fails_with: error={error!r}")
+ logging.debug(f"daemon_start_fails_with: message={message!r}")
+ assert message.lower() in error.lower()
+
+
+def daemon_get_stdout(ctx, name):
+ return _daemon_get_output(ctx, name, "stdout")
+
+
+def daemon_get_stderr(ctx, name):
+ return _daemon_get_output(ctx, name, "stderr")
+
+
+def _daemon_get_output(ctx, name, which):
+ ns = ctx.declare("_daemon")
+ this = ns[name]
+ filename = this[which]
+ data = open(filename).read()
+ logging.debug(f"Read {which} of daemon {name} from {filename}: {data!r}")
+ return data
+
+
+def daemon_has_produced_output(ctx, name=None):
+ started = time.time()
+ timeout = 5.0
+ while time.time() < started + timeout:
+ stdout = daemon_get_stdout(ctx, name)
+ stderr = daemon_get_stderr(ctx, name)
+ if stdout and stderr:
+ break
+ time.sleep(0.1)
+
+
+def daemon_stdout_is(ctx, name=None, text=None):
+ daemon_get_stdout = globals()["daemon_get_stdout"]
+ _daemon_output_is(ctx, name, text, daemon_get_stdout)
+
+
+def daemon_stderr_is(ctx, name=None, text=None):
+ daemon_get_stderr = globals()["daemon_get_stderr"]
+ _daemon_output_is(ctx, name, text, daemon_get_stderr)
+
+
+def _daemon_output_is(ctx, name, text, getter):
+ assert_eq = globals()["assert_eq"]
+ text = bytes(text, "UTF-8").decode("unicode_escape")
+ output = getter(ctx, name)
+ assert_eq(text, output)
diff --git a/subplot/vendored/daemon.yaml b/subplot/vendored/daemon.yaml
new file mode 100644
index 0000000..4fab1f6
--- /dev/null
+++ b/subplot/vendored/daemon.yaml
@@ -0,0 +1,37 @@
+- given: there is no "{args:text}" process
+ function: daemon_no_such_process
+
+- given: a daemon helper shell script {filename}
+ function: _daemon_shell_script
+
+- when: I start "{path}{args:text}" as a background process as {name}, on port {port}
+ function: daemon_start_on_port
+
+- when: I try to start "{path}{args:text}" as {name}, on port {port}
+ function: _daemon_start_soonish
+ cleanup: _daemon_stop_soonish
+
+- when: I start "{path}{args:text}" as a background process as {name}
+ function: _daemon_start
+
+- when: I stop background process {name}
+ function: daemon_stop
+
+- when: daemon {name} has produced output
+ function: daemon_has_produced_output
+
+- then: a process "{args:text}" is running
+ function: daemon_process_exists
+
+- then: there is no "{args:text}" process
+ function: daemon_no_such_process
+
+- then: starting daemon fails with "{message:text}"
+ function: daemon_start_fails_with
+
+- then: daemon {name} stdout is "{text:text}"
+ function: daemon_stdout_is
+
+- then: daemon {name} stderr is "{text:text}"
+ function: daemon_stderr_is
+
diff --git a/subplot/vendored/files.py b/subplot/vendored/files.py
new file mode 100644
index 0000000..ec37b9d
--- /dev/null
+++ b/subplot/vendored/files.py
@@ -0,0 +1,158 @@
+import logging
+import os
+import re
+import time
+
+
+def files_create_from_embedded(ctx, filename=None):
+ files_create_from_embedded_with_other_name(
+ ctx, filename_on_disk=filename, embedded_filename=filename
+ )
+
+
+def files_create_from_embedded_with_other_name(
+ ctx, filename_on_disk=None, embedded_filename=None
+):
+ get_file = globals()["get_file"]
+ with open(filename_on_disk, "wb") as f:
+ f.write(get_file(embedded_filename))
+
+
+def files_create_from_text(ctx, filename=None, text=None):
+ with open(filename, "w") as f:
+ f.write(text)
+
+
+def files_file_exists(ctx, filename=None):
+ assert_eq = globals()["assert_eq"]
+ assert_eq(os.path.exists(filename), True)
+
+
+def files_file_does_not_exist(ctx, filename=None):
+ assert_eq = globals()["assert_eq"]
+ assert_eq(os.path.exists(filename), False)
+
+
+def files_only_these_exist(ctx, filenames=None):
+ assert_eq = globals()["assert_eq"]
+ filenames = filenames.replace(",", "").split()
+ assert_eq(set(os.listdir(".")), set(filenames))
+
+
+def files_file_contains(ctx, filename=None, data=None):
+ assert_eq = globals()["assert_eq"]
+ with open(filename, "rb") as f:
+ actual = f.read()
+ actual = actual.decode("UTF-8")
+ assert_eq(data in actual, True)
+
+
+def files_file_matches_regex(ctx, filename=None, regex=None):
+ assert_eq = globals()["assert_eq"]
+ with open(filename) as f:
+ content = f.read()
+ m = re.search(regex, content)
+ if m is None:
+ logging.debug(f"files_file_matches_regex: no match")
+ logging.debug(f" filenamed: {filename}")
+ logging.debug(f" regex: {regex}")
+ logging.debug(f" content: {regex}")
+ logging.debug(f" match: {m}")
+ assert_eq(bool(m), True)
+
+
+def files_match(ctx, filename1=None, filename2=None):
+ assert_eq = globals()["assert_eq"]
+ with open(filename1, "rb") as f:
+ data1 = f.read()
+ with open(filename2, "rb") as f:
+ data2 = f.read()
+ assert_eq(data1, data2)
+
+
+def files_touch_with_timestamp(
+ ctx,
+ filename=None,
+ year=None,
+ month=None,
+ day=None,
+ hour=None,
+ minute=None,
+ second=None,
+):
+ t = (
+ int(year),
+ int(month),
+ int(day),
+ int(hour),
+ int(minute),
+ int(second),
+ -1,
+ -1,
+ -1,
+ )
+ ts = time.mktime(t)
+ _files_touch(filename, ts)
+
+
+def files_touch(ctx, filename=None):
+ _files_touch(filename, None)
+
+
+def _files_touch(filename, ts):
+ if not os.path.exists(filename):
+ open(filename, "w").close()
+ times = None
+ if ts is not None:
+ times = (ts, ts)
+ os.utime(filename, times=times)
+
+
+def files_mtime_is_recent(ctx, filename=None):
+ st = os.stat(filename)
+ age = abs(st.st_mtime - time.time())
+ assert age < 1.0
+
+
+def files_mtime_is_ancient(ctx, filename=None):
+ st = os.stat(filename)
+ age = abs(st.st_mtime - time.time())
+ year = 365 * 24 * 60 * 60
+ required = 39 * year
+ logging.debug(f"ancient? mtime={st.st_mtime} age={age} required={required}")
+ assert age > required
+
+
+def files_remember_metadata(ctx, filename=None):
+ meta = _files_remembered(ctx)
+ meta[filename] = _files_get_metadata(filename)
+ logging.debug("files_remember_metadata:")
+ logging.debug(f" meta: {meta}")
+ logging.debug(f" ctx: {ctx}")
+
+
+# Check that current metadata of a file is as stored in the context.
+def files_has_remembered_metadata(ctx, filename=None):
+ assert_eq = globals()["assert_eq"]
+ meta = _files_remembered(ctx)
+ logging.debug("files_has_remembered_metadata:")
+ logging.debug(f" meta: {meta}")
+ logging.debug(f" ctx: {ctx}")
+ assert_eq(meta[filename], _files_get_metadata(filename))
+
+
+def files_has_different_metadata(ctx, filename=None):
+ assert_ne = globals()["assert_ne"]
+ meta = _files_remembered(ctx)
+ assert_ne(meta[filename], _files_get_metadata(filename))
+
+
+def _files_remembered(ctx):
+ ns = ctx.declare("_files")
+ return ns.get("remembered-metadata", {})
+
+
+def _files_get_metadata(filename):
+ st = os.lstat(filename)
+ keys = ["st_dev", "st_gid", "st_ino", "st_mode", "st_mtime", "st_size", "st_uid"]
+ return {key: getattr(st, key) for key in keys}
diff --git a/subplot/vendored/files.yaml b/subplot/vendored/files.yaml
new file mode 100644
index 0000000..be69920
--- /dev/null
+++ b/subplot/vendored/files.yaml
@@ -0,0 +1,62 @@
+- given: file {filename}
+ function: files_create_from_embedded
+ types:
+ filename: file
+
+- given: file {filename_on_disk} from {embedded_filename}
+ function: files_create_from_embedded_with_other_name
+ types:
+ embedded_filename: file
+
+- given: file {filename} has modification time {year}-{month}-{day} {hour}:{minute}:{second}
+ function: files_touch_with_timestamp
+
+- when: I write "(?P<text>.*)" to file (?P<filename>\S+)
+ regex: true
+ function: files_create_from_text
+
+- when: I remember metadata for file {filename}
+ function: files_remember_metadata
+
+- when: I touch file {filename}
+ function: files_touch
+
+- then: file {filename} exists
+ function: files_file_exists
+
+- then: file {filename} does not exist
+ function: files_file_does_not_exist
+
+- then: only files (?P<filenames>.+) exist
+ function: files_only_these_exist
+ regex: true
+
+- then: file (?P<filename>\S+) contains "(?P<data>.*)"
+ regex: true
+ function: files_file_contains
+
+- then: file (?P<filename>\S+) matches regex /(?P<regex>.*)/
+ regex: true
+ function: files_file_matches_regex
+
+- then: file (?P<filename>\S+) matches regex "(?P<regex>.*)"
+ regex: true
+ function: files_file_matches_regex
+
+- then: files {filename1} and {filename2} match
+ function: files_match
+
+- then: file {filename} has same metadata as before
+ function: files_has_remembered_metadata
+
+- then: file {filename} has different metadata from before
+ function: files_has_different_metadata
+
+- then: file {filename} has changed from before
+ function: files_has_different_metadata
+
+- then: file {filename} has a very recent modification time
+ function: files_mtime_is_recent
+
+- then: file {filename} has a very old modification time
+ function: files_mtime_is_ancient
diff --git a/subplot/runcmd.py b/subplot/vendored/runcmd.py
index 532b60b..a2564c6 100644
--- a/subplot/runcmd.py
+++ b/subplot/vendored/runcmd.py
@@ -49,7 +49,16 @@ def runcmd_get_argv(ctx):
# ctx context.
def runcmd_run(ctx, argv, **kwargs):
ns = ctx.declare("_runcmd")
+
+ # The Subplot Python template empties os.environ at startup, modulo a small
+ # number of variables with carefully chosen values. Here, we don't need to
+ # care about what those variables are, but we do need to not overwrite
+ # them, so we just add anything in the env keyword argument, if any, to
+ # os.environ.
env = dict(os.environ)
+ for key, arg in kwargs.pop("env", {}).items():
+ env[key] = arg
+
pp = ns.get("path-prefix")
if pp:
env["PATH"] = pp + ":" + env["PATH"]
diff --git a/subplot/runcmd.yaml b/subplot/vendored/runcmd.yaml
index 48dde90..48dde90 100644
--- a/subplot/runcmd.yaml
+++ b/subplot/vendored/runcmd.yaml