summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xcheck9
-rwxr-xr-xscripts/yaml-extract74
-rw-r--r--yarns/branches-workspaces.yarn283
-rw-r--r--yarns/implementations.yarn356
-rw-r--r--yarns/morph.shell-lib153
5 files changed, 875 insertions, 0 deletions
diff --git a/check b/check
index f4391e08..975bfdcb 100755
--- a/check
+++ b/check
@@ -48,6 +48,15 @@ export PYTHONPATH
python setup.py clean check
+# Run scenario tests with yarn, if yarn is available.
+
+if command -v yarn > /dev/null
+then
+ yarn -s yarns/morph.shell-lib yarns/*.yarn
+fi
+
+# cmdtest tests.
+
HOME="$(pwd)/scripts"
cmdtest tests
diff --git a/scripts/yaml-extract b/scripts/yaml-extract
new file mode 100755
index 00000000..5a945b6d
--- /dev/null
+++ b/scripts/yaml-extract
@@ -0,0 +1,74 @@
+#!/usr/bin/python
+#
+# Extract field from YAML format morphologies, using a very simple
+# query language. This is useful for black box testing.
+#
+# Usage: yaml-extract FILE PARAM...
+#
+# Where FILE is the name of the YAML morphology, and PARAM are a sequence
+# of query parameters.
+#
+# The program reads in the YAML file, and then selects successively deeper
+# parts of the object hieararchy in the file. If the object currently
+# being looked at is a dictionary, PARAM is a field in the dictionary,
+# and the next PARAM will look at the value stored with that key.
+# If the current object is a list, PARAM can either be an integer list
+# index, or a search key of the form KEY=VALUE, in which case the list
+# is searched for the first member, which must be a dict, which has
+# a key KEY that stores a value VALUE.
+#
+# Example:
+#
+# yaml-extract system.morph strata morph=core ref
+#
+# This would report the ref of the core stratum in a system.
+#
+# Note that this does not try to parse morphologies as morphologies,
+# and so doesn't do special processing such as providing computed
+# values for missing fields (e.g., the morph field if name is given).
+# Construct your tests accordingly.
+
+# Copyright (C) 2013 Codethink Limited
+#
+# 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; version 2 of the License.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+import sys
+import yaml
+
+
+with open(sys.argv[1]) as f:
+ obj = yaml.safe_load(f)
+for thing in sys.argv[2:]:
+ if type(obj) == dict:
+ obj = obj[thing]
+ elif type(obj) == list:
+ if '=' in thing:
+ # We need to search a list member dict with a given field.
+ key, value = thing.split('=', 1)
+ for item in obj:
+ if item.get(key) == value:
+ obj = item
+ break
+ else:
+ raise Exception(
+ "Couldn't find list item containing %s" % thing)
+ else:
+ # We can just index.
+ obj = obj[int(thing)]
+ else:
+ raise Exception("Can't handle %s with %s" % (repr(obj), repr(thing)))
+
+print obj
+
diff --git a/yarns/branches-workspaces.yarn b/yarns/branches-workspaces.yarn
new file mode 100644
index 00000000..bfcb9e66
--- /dev/null
+++ b/yarns/branches-workspaces.yarn
@@ -0,0 +1,283 @@
+Morph black box tests for system branches and workspaces
+========================================================
+
+Morph implements **system branches**, which are checked out and
+manipulated by the user in **workspaces**. See
+FIXME for more information.
+
+Workspace creation
+------------------
+
+The first thing a user needs to do is create a workspace.
+
+ SCENARIO create and initialise a new workspace
+ GIVEN no workspace
+ WHEN morph initialises a workspace
+ THEN an empty workspace exists
+
+The workspace directory may exist, if it is empty.
+
+ SCENARIO initialise an empty workspace directory
+ GIVEN an empty workspace directory
+ WHEN morph initialises a workspace
+ THEN an empty workspace exists
+
+However, the directory must really be empty. It must not be
+an empty, but initialised workspace.
+
+ SCENARIO initialise an existing, empty workspace directory
+ GIVEN no workspace
+ WHEN morph initialises a workspace
+ AND morph attempts to initialise a workspace
+ THEN morph failed
+
+Likewise, if the directory exists, and is non-empty, but isn't an
+existing workspace, initialising it should fail.
+
+ SCENARIO initialise a non-empty workspace directory
+ GIVEN a non-empty workspace directory
+ WHEN morph attempts to initialise a workspace
+ THEN morph failed
+
+Checking out or branching system branches
+-----------------------------------------
+
+Once we have a workspace, we can check out a system branch.
+
+ SCENARIO check out an existing system branch
+ GIVEN a workspace
+ AND a git server
+ WHEN checking out the master system branch
+ THEN the system branch master is checked out
+
+Checking out a system branch should fail, if the branch doesn't exist.
+
+ SCENARIO checking out a system branch that doesn't exist
+ GIVEN a workspace
+ AND a git server
+ WHEN morph attempts to check out system branch foo
+ THEN morph failed
+
+We can, instead, create a new system branch, off master.
+
+ SCENARIO branch off master
+ GIVEN a workspace
+ AND a git server
+ WHEN creating system branch foo
+ THEN the system branch foo is checked out
+
+We can also branch off another system branch. However, we need to first
+push the other branch to the git server, since Morph is not smart enough
+to check for that locally.
+
+ SCENARIO branch off non-master
+ GIVEN a workspace
+ AND a git server
+ WHEN creating system branch foo
+ AND pushing system branch foo to git server
+ AND creating system branch bar, based on foo
+ THEN the system branch bar is checked out
+
+Query commands in workspaces
+----------------------------
+
+`morph workspace` writes out the fully qualified path to the workspace
+directory, regardless of where the user is. There's a few cases.
+
+ SCENARIO morph workspace works at root of empty workspace
+ GIVEN a workspace
+ WHEN morph reports workspace in .
+ THEN workspace is reported correctly
+
+Also check it in the root of a system branch checkout, and inside
+a git checkout inside that.
+
+ SCENARIO morph workspace works in system branch checkouts
+ GIVEN a workspace
+ AND a git server
+ WHEN checking out the master system branch
+ AND morph reports workspace in master
+ THEN workspace is reported correctly
+
+We leak a little bit of the implementation here, to keep things simple:
+the (mocked) git server the implementation sets up has the `test:morphs`
+repository, which is the system branch root repository.
+
+ WHEN morph reports workspace in master/test:morphs
+ THEN workspace is reported correctly
+
+However, running it outside a workspace should fail.
+
+ SCENARIO morph fails outside workspace
+ GIVEN no workspace
+ WHEN morph attempts to report workspace
+ THEN morph failed
+
+`morph show-system-branch` should report the name of the system
+branch, when run anywhere in the system branch checkout. As a special
+case, if there is only one system branch checkout at or below the
+current working directory, it will find it and report it correctly.
+
+ SCENARIO morph reports system branch
+ GIVEN a workspace
+ AND a git server
+ WHEN checking out the master system branch
+ AND reporting system branch in master
+ THEN system branch is reported as master
+
+ WHEN reporting system branch in master/test:morphs
+ THEN system branch is reported as master
+
+ WHEN reporting system branch in .
+ THEN system branch is reported as master
+
+However, if there's two system branches checked out below the
+current directory, things should fail.
+
+ SCENARIO morph fails to report system branch with two checked out
+ GIVEN a workspace
+ AND a git server
+ WHEN checking out the master system branch
+ AND creating system branch foo
+ AND attempting to report system branch in .
+ THEN morph failed
+
+`morph show-branch-root` reports the URL (possibly aliases) of the
+system branch root repository. It can be run inside a checkout, or
+somewhere outside a checkout, where exactly one checkout exists below.
+
+ SCENARIO morph reports system branch root repository
+ GIVEN a workspace
+ AND a git server
+ WHEN checking out the master system branch
+ AND reporting system branch root repository in master
+ THEN root repository is reported as test:morphs
+
+ WHEN reporting system branch root repository in .
+ THEN root repository is reported as test:morphs
+
+However, it fails if run outside a checkout and there's no system
+branches checked out.
+
+ SCENARIO morph fails to report system branch with two checked out
+ GIVEN a workspace
+ AND a git server
+ WHEN attempting to report system branch root repository in .
+ THEN morph failed
+
+Editing components
+------------------
+
+`morph edit` can edit refs for a stratum only, or it can do that for
+a chunk, and check out the chunk's repository.
+
+First of all, we verify that that when we create a system branch,
+all the refs are unchanged.
+
+ SCENARIO morph branch does not edit refs
+ GIVEN a workspace
+ AND a git server
+ WHEN creating system branch foo
+ THEN in branch foo, system test-system refs test-stratum in master
+ AND in branch foo, stratum test-stratum refs test-chunk in master
+
+Then edit the stratum.
+
+ WHEN editing stratum test-stratum in system test-system in branch foo
+ THEN in branch foo, system test-system refs test-stratum in foo
+
+Edit the chunk. We make use of special knowledge here: `test:test-chunk`
+is a chunk repository created in the mocked git server, for testing
+purposes.
+
+ WHEN editing chunk test-chunk in test-stratum in test-system in branch foo
+ THEN in branch foo, system test-system refs test-stratum in foo
+ AND in branch foo, stratum test-stratum refs test-chunk in foo
+ AND edited chunk test:test-chunk has git branch foo
+
+Status of system branch checkout
+--------------------------------
+
+`morph status` shows the status of all git repositories in a
+system branch checkout: only the ones that exist locally, not all the
+repositories referenced in the system branch.
+
+ SCENARIO morph status reports changes correctly
+ GIVEN a workspace
+ AND a git server
+ WHEN creating system branch foo
+ THEN morph reports no outstanding changes in foo
+
+ WHEN editing stratum test-stratum in system test-system in branch foo
+ THEN morph reports changes in foo in test:morphs only
+
+ WHEN editing chunk test-chunk in test-stratum in test-system in branch foo
+ THEN morph reports changes in foo in test:morphs only
+
+ WHEN creating file foo in test:test-chunk in branch foo
+ THEN morph reports changes in foo in test:morphs only
+
+ WHEN adding file foo in test:test-chunk in branch foo to git
+ THEN morph reports changes in foo in test:morphs and test:test-chunk
+
+ WHEN committing changes in test:morphs in branch foo
+ THEN morph reports changes in foo in test:test-chunk only
+
+ WHEN committing changes in test:test-chunk in branch foo
+ THEN morph reports no outstanding changes in foo
+
+`morph foreach`
+--------------
+
+`morph foreach` runs a shell command in each of the git repos in a system
+branch checkout.
+
+ SCENARIO morph foreach runs command in each git repo
+ GIVEN a workspace
+ AND a git server
+ WHEN creating system branch foo
+ AND editing chunk test-chunk in test-stratum in test-system in branch foo
+ AND running shell command in each repo in foo
+ THEN morph ran command in test:morphs in foo
+ AND morph ran command in test:test-chunk in foo
+
+Explicit petrification
+----------------------
+
+We petrify branches explicitly (though this may later change so that
+`morph branch` does it automatically). To test this, we create a branch,
+petrify it, and verify that every ref looks like a SHA1. We then
+unpetrify and verify that we have all the same refs as before.
+
+ SCENARIO morph petrifies and unpetrifies
+ GIVEN a workspace
+ AND a git server
+ WHEN creating system branch foo
+ AND pushing system branch foo to git server
+ AND remembering all refs in foo
+ AND petrifying foo
+ THEN foo is petrified
+ WHEN unpetrifying foo
+ THEN foo refs are as remembered
+
+Tagging system branches
+-----------------------
+
+`morph tag` creates a git tag in the system branch's root repository,
+and a petrified commit the tag refers to. It does not petrify the
+system branch itself, only the tag.
+
+ SCENARIO morph tags a system branch
+ GIVEN a workspace
+ AND a git server
+ WHEN creating system branch foo
+ AND tagging system branch foo as test123
+ THEN morph tag test123 in foo is an annotated git tag
+ AND morph tag test123 in foo refers to a petrified commit
+ AND foo is not petrified
+
+Creating a tag twice should fail.
+
+ WHEN attempting to tag system branch foo as test123
+ THEN morph failed
+
diff --git a/yarns/implementations.yarn b/yarns/implementations.yarn
new file mode 100644
index 00000000..6c0ef6e2
--- /dev/null
+++ b/yarns/implementations.yarn
@@ -0,0 +1,356 @@
+IMPLEMENTS implementations
+==========================
+
+Implementation sections for workspaces
+--------------------------------------
+
+We'll use `$DATADIR/workspace` as the workspace directory that is used.
+
+ IMPLEMENTS GIVEN no workspace
+ true
+
+ IMPLEMENTS GIVEN an empty workspace directory
+ mkdir "$DATADIR/workspace"
+
+ IMPLEMENTS GIVEN a non-empty workspace directory
+ mkdir "$DATADIR/workspace"
+ touch "$DATADIR/workspace/random-file"
+
+We run `morph init` in two different ways: either the simple way,
+letting yarn catch errors, or in a way that catches the error so
+we can test it later in a THEN step.
+
+ IMPLEMENTS WHEN morph initialises a workspace
+ run_morph init "$DATADIR/workspace"
+
+ IMPLEMENTS WHEN morph attempts to initialise a workspace
+ attempt_morph init "$DATADIR/workspace"
+
+ IMPLEMENTS THEN morph failed
+ case $(cat "$DATADIR/morph-exit") in
+ 0) die "Morph should have failed, but didn't. Unexpected success!" ;;
+ esac
+
+We need to check that a workspace creation worked. This requires the
+directory to exist, and its `.morph` subdirectory to exist, and nothing
+else.
+
+ IMPLEMENTS THEN an empty workspace exists
+ is_dir "$DATADIR/workspace"
+ is_dir "$DATADIR/workspace/.morph"
+ assert_equal $(ls -A "$DATADIR/workspace" | wc -l) 1
+
+Tests for things other than `morph init` just want to have a workspace
+created.
+
+ IMPLEMENTS GIVEN a workspace
+ run_morph init "$DATADIR/workspace"
+
+Implementation sections related to a simulated Trove
+----------------------------------------------------
+
+Morph needs access to a Trove, i.e., a git server, in order to do certain
+kinds of stuff. We simulate this by creating a set of git repositories
+locally, which we'll tell Morph to access using `file:` URLs. Specifically,
+we'll create a repository to hold system and stratum morphologies, and
+another to hold a chunk.
+
+ IMPLEMENTS GIVEN a git server
+
+ # Create a directory for all the git repositories.
+ mkdir "$DATADIR/gits"
+
+ # Create a repo for the system and stratum morphologies.
+
+ mkdir "$DATADIR/gits/morphs"
+
+ cat << EOF > "$DATADIR/gits/morphs/test-system.morph"
+ name: test-system
+ kind: system
+ strata:
+ - name: test-stratum
+ repo: test:morphs
+ ref: master
+ morph: test-stratum
+ EOF
+
+ cat << EOF > "$DATADIR/gits/morphs/test-stratum.morph"
+ name: test-stratum
+ kind: stratum
+ chunks:
+ - name: test-chunk
+ repo: test:test-chunk
+ ref: master
+ morph: test-chunk
+ EOF
+
+ run_in "$DATADIR/gits/morphs" git init .
+ run_in "$DATADIR/gits/morphs" git add .
+ run_in "$DATADIR/gits/morphs" git commit -m Initial.
+
+ # Create the chunk repository.
+
+ mkdir "$DATADIR/gits/test-chunk"
+
+ cat << EOF > "$DATADIR/gits/test-chunk/test-chunk.morph"
+ name: test-chunk
+ kind: chunk
+ build-system: dummy
+ EOF
+
+ run_in "$DATADIR/gits/test-chunk" git init .
+ run_in "$DATADIR/gits/test-chunk" git add .
+ run_in "$DATADIR/gits/test-chunk" git commit -m Initial.
+
+ # Create the Morph configuration file so we can access the repos
+ # using test:foo URL aliases.
+
+ cat << EOF > "$DATADIR/morph.conf"
+ [config]
+ repo-alias = test=file://$DATADIR/gits/%s#file://$DATADIR/gits/%s
+ EOF
+
+Implementation sections for system branch operations
+----------------------------------------------------
+
+Checkout out an existing system branch. We parameterise this so the
+same phrase can be used to check out any system branch.
+
+ IMPLEMENTS WHEN checking out the (\S+) system branch
+ cd "$DATADIR/workspace"
+ run_morph checkout test:morphs "$MATCH_1"
+
+Attempt to check out a system branch, and remember if it failed.
+
+ IMPLEMENTS WHEN morph attempts to check out system branch (\S+)
+ cd "$DATADIR/workspace"
+ attempt_morph checkout "$MATCH_1"
+
+We also need to verify that a system branch has been checked out.
+
+ IMPLEMENTS THEN the system branch (\S+) is checked out
+ is_dir "$DATADIR/workspace/$MATCH_1/test:morphs"
+ is_file "$DATADIR/workspace/$MATCH_1/test:morphs/test-system.morph"
+ is_file "$DATADIR/workspace/$MATCH_1/test:morphs/test-stratum.morph"
+
+We can create a new branch, off master.
+
+ IMPLEMENTS WHEN creating system branch (\S+)
+ cd "$DATADIR/workspace"
+ run_morph branch test:morphs "$MATCH_1"
+
+We can create a new branch, off another system branch.
+
+ IMPLEMENTS WHEN creating system branch (\S+), based on (\S+)
+ cd "$DATADIR/workspace"
+ run_morph branch test:morphs "$MATCH_1" "$MATCH_2"
+
+Pushing all changes in a system branch checkout to the git server.
+
+ IMPLEMENTS WHEN pushing system branch (\S+) to git server
+ # FIXME: For now, this is just the morphs checkout.
+ run_in "$DATADIR/workspace/$MATCH_1/test:morphs" git push origin HEAD
+
+Report workspace path.
+
+ IMPLEMENTS WHEN morph reports workspace in (\S+)
+ cd "$DATADIR/workspace/$MATCH_1"
+ run_morph workspace > "$DATADIR/workspace-reported"
+
+ IMPLEMENTS THEN workspace is reported correctly
+ assert_equal $(cat "$DATADIR/workspace-reported") "$DATADIR/workspace"
+
+ IMPLEMENTS WHEN morph attempts to report workspace
+ cd "$DATADIR"
+ attempt_morph workspace
+
+Report system branch name:
+
+ IMPLEMENTS WHEN reporting system branch in (\S+)
+ cd "$DATADIR/workspace/$MATCH_1"
+ run_morph show-system-branch > "$DATADIR/system-branch.reported"
+
+ IMPLEMENTS THEN system branch is reported as (.*)
+ echo "$MATCH_1" > "$DATADIR/system-branch.actual"
+ diff -u "$DATADIR/system-branch.actual" "$DATADIR/system-branch.reported"
+
+ IMPLEMENTS WHEN attempting to report system branch in (.*)
+ cd "$DATADIR/workspace/$MATCH_1"
+ attempt_morph show-system-branch
+
+Report system branch root repository.
+
+ IMPLEMENTS WHEN reporting system branch root repository in (.*)
+ cd "$DATADIR/workspace/$MATCH_1"
+ run_morph show-branch-root > "$DATADIR/branch-root.reported"
+
+ IMPLEMENTS THEN root repository is reported as (.*)
+ echo "$MATCH_1" > "$DATADIR/branch-root.actual"
+ diff -u "$DATADIR/branch-root.actual" "$DATADIR/branch-root.reported"
+
+ IMPLEMENTS WHEN attempting to report system branch root repository in (.*)
+ cd "$DATADIR/workspace/$MATCH_1"
+ attempt_morph show-branch-root
+
+Editing morphologies with `morph edit`.
+
+ IMPLEMENTS THEN in branch (\S+), system (\S+) refs (\S+) in (\S+)
+ "$SRCDIR/scripts/yaml-extract" \
+ "$DATADIR/workspace/$MATCH_1/test:morphs/$MATCH_2.morph" \
+ strata name="$MATCH_3" ref > "$DATADIR/ref.actual"
+ echo "$MATCH_4" > "$DATADIR/ref.wanted"
+ diff -u "$DATADIR/ref.wanted" "$DATADIR/ref.actual"
+
+ IMPLEMENTS THEN in branch (\S+), stratum (\S+) refs (\S+) in (\S+)
+ "$SRCDIR/scripts/yaml-extract" \
+ "$DATADIR/workspace/$MATCH_1/test:morphs/$MATCH_2.morph" \
+ chunks name="$MATCH_3" ref > "$DATADIR/ref.actual"
+ echo "$MATCH_4" > "$DATADIR/ref.wanted"
+ diff -u "$DATADIR/ref.wanted" "$DATADIR/ref.actual"
+
+ IMPLEMENTS WHEN editing stratum (\S+) in system (\S+) in branch (\S+)
+ cd "$DATADIR/workspace/$MATCH_3/test:morphs"
+ run_morph edit "$MATCH_2" "$MATCH_1"
+
+ IMPLEMENTS WHEN editing chunk (\S+) in (\S+) in (\S+) in branch (\S+)
+ cd "$DATADIR/workspace/$MATCH_4/test:morphs"
+ run_morph edit "$MATCH_3" "$MATCH_2" "$MATCH_1"
+
+ IMPLEMENTS THEN edited chunk (\S+) has git branch (\S+)
+ ls -l "$DATADIR/workspace/$MATCH_2"
+ cd "$DATADIR/workspace/$MATCH_2/$MATCH_1"
+ git branch | awk '$1 == "*" { print $2 }' > "$DATADIR/git-branch.actual"
+ echo "$MATCH_2" > "$DATADIR/git-branch.wanted"
+ diff -u "$DATADIR/git-branch.wanted" "$DATADIR/git-branch.actual"
+
+Reporting status of checked out repositories:
+
+ IMPLEMENTS THEN morph reports no outstanding changes in (\S+)
+ cd "$DATADIR/workspace/$MATCH_1"
+ run_morph status > "$DATADIR/morph.stdout"
+ grep '^No repos have outstanding changes.' "$DATADIR/morph.stdout"
+
+ IMPLEMENTS THEN morph reports changes in (\S+) in (\S+) only
+ cd "$DATADIR/workspace/$MATCH_1"
+ run_morph status > "$DATADIR/morph.stdout"
+
+ # morph status is expected to produce records like this:
+ # On branch GITBRANCH, root baserock:baserock/morphs
+ # GITREPO: uncommitted changes
+ # We check thet GITREPO matches $MATCH_2.
+
+ awk '/: uncommitted changes$/ { print substr($1,1,length($1)-1) }' \
+ "$DATADIR/morph.stdout" > "$DATADIR/changed.actual"
+ echo "$MATCH_2" > "$DATADIR/changed.wanted"
+ diff -u "$DATADIR/changed.wanted" "$DATADIR/changed.actual"
+
+ IMPLEMENTS THEN morph reports changes in (\S+) in (\S+) and (\S+)
+ cd "$DATADIR/workspace/$MATCH_1"
+ run_morph status > "$DATADIR/morph.stdout"
+ echo "status morph.stdout:"
+ cat "$DATADIR/morph.stdout"
+ awk '/: uncommitted changes$/ { print substr($1,1,length($1)-1) }' \
+ "$DATADIR/morph.stdout" | sort > "$DATADIR/changed.actual"
+ (echo "$MATCH_2"; echo "$MATCH_3") | sort > "$DATADIR/changed.wanted"
+ diff -u "$DATADIR/changed.wanted" "$DATADIR/changed.actual"
+
+ IMPLEMENTS WHEN creating file (\S+) in (\S+) in branch (\S+)
+ touch "$DATADIR/workspace/$MATCH_3/$MATCH_2/$MATCH_1"
+
+ IMPLEMENTS WHEN adding file (\S+) in (\S+) in branch (\S+) to git
+ cd "$DATADIR/workspace/$MATCH_3/$MATCH_2"
+ git add "$MATCH_1"
+
+ IMPLEMENTS WHEN committing changes in (\S+) in branch (\S+)
+ cd "$DATADIR/workspace/$MATCH_2/$MATCH_1"
+ git commit -a -m test-commit
+
+Running shell command in each checked out repository:
+
+ IMPLEMENTS WHEN running shell command in each repo in (\S+)
+ cd "$DATADIR/workspace/$MATCH_1"
+ run_morph foreach -- pwd > "$DATADIR/morph.stdout"
+
+ IMPLEMENTS THEN morph ran command in (\S+) in (\S+)
+ grep -Fx "$MATCH_1" "$DATADIR/morph.stdout"
+ grep -Fx "$DATADIR/workspace/$MATCH_2/$MATCH_1" "$DATADIR/morph.stdout"
+
+Petrification and unpetrification:
+
+ IMPLEMENTS WHEN remembering all refs in (\S+)
+ cd "$DATADIR/workspace/$MATCH_1/test:morphs"
+ list_refs *.morph > "$DATADIR/refs.remembered"
+
+ IMPLEMENTS THEN (\S+) refs are as remembered
+ cd "$DATADIR/workspace/$MATCH_1/test:morphs"
+
+ # FIXME: petrify/unpetrify doesn't work quite right at this time:
+ # petrify can change a ref to a stratum to point at the system
+ # branch, but does it without adding an unpetrify-ref, and so
+ # unpetrify doesn't undo the change. We ignore this bug for the
+ # time being, in order to make the test suite pass. When the
+ # petrification code has been cleaned up to not be so hairy,
+ # we'll fix the test and the code.
+ #
+ # We would like to verify the result like this:
+ #
+ # list_refs *.morph > "$DATADIR/refs.now"
+ # diff -u "$DATADIR/refs.remembered" "$DATADIR/refs.now"
+ #
+ # However, due to the bug, we have to do it in a more complicated
+ # manner.
+
+ list_refs *.morph |
+ while read filename ref
+ do
+ orig=$(awk -v "f=$filename" '$1 == f { print $2 }' \
+ "$DATADIR/refs.remembered")
+ if [ "$orig" != "$ref" ] && [ "$ref" != "$MATCH_1" ]
+ then
+ die "Un-petrified ref: $filename $ref (should be $orig)"
+ fi
+ done
+
+ IMPLEMENTS WHEN petrifying (\S+)
+ cd "$DATADIR/workspace/$MATCH_1/test:morphs"
+ run_morph petrify
+
+ IMPLEMENTS WHEN unpetrifying (\S+)
+ cd "$DATADIR/workspace/$MATCH_1/test:morphs"
+ run_morph unpetrify
+
+ IMPLEMENTS THEN (\S+) is petrified
+ cd "$DATADIR/workspace/$MATCH_1/test:morphs"
+ assert_morphologies_are_petrified "$MATCH_1" *.morph
+
+ IMPLEMENTS THEN (\S+) is not petrified
+ cd "$DATADIR/workspace/$MATCH_1/test:morphs"
+ ! assert_morphologies_are_petrified "$MATCH_1" *.morph
+
+Tagging.
+
+ IMPLEMENTS WHEN tagging system branch (\S+) as (\S+)
+ cd "$DATADIR/workspace/$MATCH_1/test:morphs"
+ run_morph tag "$MATCH_2" -- -m "testing morph tagging"
+
+ IMPLEMENTS WHEN attempting to tag system branch (\S+) as (\S+)
+ cd "$DATADIR/workspace/$MATCH_1/test:morphs"
+ attempt_morph tag "$MATCH_2" -- -m "testing morph tagging"
+
+ IMPLEMENTS THEN morph tag (\S+) in (\S+) is an annotated git tag
+ cd "$DATADIR/workspace/$MATCH_2/test:morphs"
+ if git show "$MATCH_1" | head -n1 | grep -v '^tag '
+ then
+ die "git tag $MATCH_1 is not an annotated tag"
+ fi
+
+ IMPLEMENTS THEN morph tag (\S+) in (\S+) refers to a petrified commit
+ cd "$DATADIR/workspace/$MATCH_2/test:morphs"
+ git ls-tree "$MATCH_1" |
+ awk '$NF ~ /\.morph$/ { print $NF }' |
+ while read x
+ do
+ git cat-file blob "$MATCH_1:$x" > temptemptemp
+ assert_morphologies_are_petrified "$MATCH_1" temptemptemp
+ done
+
diff --git a/yarns/morph.shell-lib b/yarns/morph.shell-lib
new file mode 100644
index 00000000..4fb1eb10
--- /dev/null
+++ b/yarns/morph.shell-lib
@@ -0,0 +1,153 @@
+# Shell library for Morph yarns.
+#
+# The shell functions in this library are meant to make writing IMPLEMENTS
+# sections for yarn scenario tests easier.
+
+# Copyright (C) 2013 Codethink Limited
+#
+# 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; version 2 of the License.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+# Run Morph from the source tree, ignoring any configuration files.
+# This way the test suite is not affected by any configuration the user
+# or system may have. Instead, we'll use the `$DATADIR/morph.conf` file,
+# which tests can create, if they want to. Unfortunately, currently yarn
+# does not set a $SRCDIR that points at the source tree, so if the test
+# needs to cd away from there, things can break. We work around this
+# by allowing the caller to set $SRCDIR if they want to, and if it isn't
+# set, we default to . (current working directory).
+
+run_morph()
+{
+ "${SRCDIR:-.}"/morph \
+ --no-default-config --config "$DATADIR/morph.conf" "$@"
+}
+
+
+# Sometimes we want to try running morph, but are OK if it fails, we just
+# need to remember that it did.
+
+attempt_morph()
+{
+ if run_morph "$@"
+ then
+ echo 0 > "$DATADIR/morph-exit"
+ else
+ echo "$?" > "$DATADIR/morph-exit"
+ fi
+}
+
+
+# Perl's die() function is often very useful: it prints an error message
+# and terminates the process with a non-zero exit code. Let's have a
+# shell function to do that.
+
+die()
+{
+ echo "ERROR: $@" 1>&2
+ exit 1
+}
+
+
+# Tests often need to check that specific files or directories exist
+# and have the right ownerships etc. Here's some shell functions to
+# test that kind of thing.
+
+is_dir()
+{
+ if [ ! -d "$1" ]
+ then
+ die "Expected $1 to be a directory"
+ fi
+}
+
+is_file()
+{
+ if [ ! -f "$1" ]
+ then
+ die "Expected $1 to be a regular file"
+ fi
+}
+
+
+# General assertions.
+
+assert_equal()
+{
+ if [ "$1" != "$2" ]
+ then
+ die "Expected '$1' and '$2' to be equal"
+ fi
+}
+
+
+# Sometimes it's nice to run a command in a different directory, without
+# having to bother changing the directory before and after the command,
+# or spawning subshells. This function helps with that.
+
+run_in()
+{
+ (cd "$1" && shift && exec "$@")
+}
+
+
+# Extract all refs in all given morphologies. Each ref is reported
+# as filename:ref. The referred-to repository is not listed.
+
+list_refs()
+{
+ awk '/ ref: / { printf "%s %s\n", FILENAME, $NF }' "$@"
+}
+
+
+# Is a ref petrified? Or a specific branch?
+
+is_petrified_or_branch()
+{
+ if echo "$1" |
+ awk -v "branch=$2" '$NF ~ /[0-9a-fA-F]{40}/ || $NF == branch' |
+ grep .
+ then
+ return 0
+ else
+ return 1
+ fi
+}
+
+
+# Are named morphologies petrified? Die if not. First arg is the
+# branch that is allowed in addition to SHA1s.
+
+assert_morphologies_are_petrified()
+{
+ local branch="$1"
+ shift
+ list_refs "$@" |
+ while read filename ref
+ do
+ if ! is_petrified_or_branch "$ref" "$branch"
+ then
+ die "Found non-SHA1 ref in $filename: $ref"
+ fi
+ done
+}
+
+
+# Currently, yarn isn't setting $SRCDIR to point at the project source
+# directory. We simulate this here.
+
+if ! env | grep '^SRCDIR=' > /dev/null
+then
+ export SRCDIR="$(pwd)"
+fi