Browse Source

Move from fastfoo version 0.7.0+master.g7fde2b3

Alois Mahdal 9 years ago
parent
commit
a9b5e304eb

+ 9
- 0
Makefile View File

@@ -0,0 +1,9 @@
1
+# ffoo - Fastfoo - Bash dot on steroids
2
+# See LICENSE file for copyright and license details.
3
+
4
+include config.mk
5
+
6
+MKIT_DIR=utils/mkit
7
+include $(MKIT_DIR)/mkit.mk
8
+
9
+.PHONY: test

+ 3
- 0
config.mk View File

@@ -0,0 +1,3 @@
1
+VERSION = 0.0.0
2
+RELSRC = master
3
+RELDST = last

+ 33
- 0
mkit.ini View File

@@ -0,0 +1,33 @@
1
+
2
+[ENV]
3
+
4
+    PKGNAME = mkit
5
+    PROJNAME = Mkit
6
+    PREFIX = utils
7
+
8
+[lists]
9
+
10
+    group = bin
11
+    group = rest
12
+
13
+    dist = config.mk
14
+    dist = LICENSE
15
+    dist = Makefile
16
+    dist = mkit.ini
17
+    dist = README.md
18
+    dist = src
19
+    dist = utils
20
+
21
+[modes]
22
+    bin = 755
23
+
24
+[roots]
25
+    bin  = [ENV:PREFIX]/mkit
26
+    rest = [ENV:PREFIX]/mkit
27
+
28
+[files:bin]
29
+    src/make     = make
30
+
31
+[files:rest]
32
+    src/include  = include
33
+    src/mkit.mk  = mkit.mk

+ 194
- 0
src/include/build.sh View File

@@ -0,0 +1,194 @@
1
+#!/bin/bash
2
+
3
+. "$MKIT_DIR/include/ini.sh" || die "cannot import ini.sh"
4
+
5
+
6
+build() {
7
+    #
8
+    # Add meat to all skeletons
9
+    #
10
+    local srcpath
11
+    find src -type f -name '*.skel' \
12
+     | while read srcpath;
13
+       do
14
+           build1 "$srcpath"
15
+       done
16
+}
17
+
18
+build1() {
19
+    #
20
+    # Process one skeleton
21
+    #
22
+    local srcpath dstpath
23
+    srcpath=$1
24
+    dstpath=${srcpath%.skel}
25
+    case $dstpath in
26
+        *.md) <"$srcpath" expand_includes | expand_variables >"$dstpath" ;;
27
+        *)    <"$srcpath"                   expand_variables >"$dstpath" ;;
28
+    esac
29
+    mkdir -p "$MKIT_LOCAL"
30
+    echo "$dstpath" >> "$MKIT_LOCAL/built.lst"
31
+}
32
+
33
+build_manpages() {
34
+    local manfile mdfile
35
+    if command -v ronn >/dev/null;
36
+    then
37
+        ini lskeys "files:man" \
38
+          | while read manfile;
39
+            do
40
+                mdfile="$manfile.md"
41
+                ronn -r "$mdfile"
42
+                mkdir -p "$MKIT_LOCAL"
43
+                echo "$manfile" >> "$MKIT_LOCAL/built.lst"
44
+            done
45
+    else
46
+        echo "ronn is not installed"
47
+        return 1
48
+    fi
49
+}
50
+
51
+clean() {
52
+    #
53
+    # Clean up tree after building
54
+    #
55
+    test -f "$MKIT_LOCAL/built.lst" && {
56
+        <"$MKIT_LOCAL/built.lst" xargs -r rm -f
57
+        rm -f "$MKIT_LOCAL/built.lst"
58
+        rmdir --ignore-fail-on-non-empty "$MKIT_LOCAL"
59
+    }
60
+    true
61
+}
62
+
63
+dist() {
64
+    #
65
+    # Create distributable tarball
66
+    #
67
+    local version=$(get_version)
68
+    local dirname=$MKIT_PKGNAME-$version
69
+    mkdir -p "$dirname"
70
+    local item
71
+    cp -R "$(ini values "lists:dist")" "$dirname"
72
+    sed -i -e "s/^VERSION = .*/VERSION = $version/" "$dirname/config.mk"
73
+    tar -cf "$dirname.tar" "$dirname"
74
+    gzip "$dirname.tar"
75
+    rm -rf "$dirname"
76
+}
77
+
78
+expand_includes() {
79
+    #
80
+    # Expand include directives
81
+    #
82
+    # Expand e.g. `<!-- include4: foo.sh -->` to include code of foo.sh
83
+    #
84
+    perl -we '
85
+        use strict;
86
+        my $text;
87
+        while (<>) {
88
+            chomp;
89
+            if (m/<!-- include4: (\S+) -->/) {
90
+                open my $fh, $1 or warn "cannot find: $1";
91
+                my $text = do { local($/); <$fh> };
92
+                close $fh;
93
+                $text =~ s/^(.)/    $1/gm;
94
+                chomp $text;
95
+                print "$text\n";
96
+            } else {
97
+                print "$_\n";
98
+            }
99
+        }
100
+    '
101
+}
102
+
103
+expand_variables() {
104
+    #
105
+    # Expand variable values
106
+    #
107
+    local script=$(mktemp --tmpdir mkit-tmp.XXXXXXXXXX)
108
+    local varname varvalue
109
+    ini lskeys "vars" \
110
+      | while read varname;
111
+        do
112
+            varvalue="$(ini 1value "vars:$varname" | sed -e 's/\$/\\$/' )"
113
+            echo "s|$varname|$varvalue|;" >> "$script"
114
+        done
115
+    echo "s|__CODENAME__|$CODENAME|;"     >> "$script"
116
+    echo "s|__VERSION__|$(get_version)|;" >> "$script"
117
+    perl -wp "$script"
118
+    rm "$script"
119
+}
120
+
121
+get_version() {
122
+    #
123
+    # Build semver version string with build metadata
124
+    #
125
+    # Build version string from available info using following
126
+    # logic:
127
+    #
128
+    #  1. use VERSION (from config.mk)
129
+    #  2. if we are in git, override the version with last tag
130
+    #  3. if set, add PRERELEASE (from config.mk) as pre-release ID
131
+    #     (afer dash)
132
+    #  4. if we are at a later commit than the last tag, add branch
133
+    #     name and commit sha1 to build metadata (after plus sign)
134
+    #  5. if the tree is "dirty", i.e. has uncommited changes,
135
+    #     add "dirty" to build metadata
136
+    #
137
+    # The version is compatible with SemVer 2.0.0.
138
+    #
139
+    # Examples:
140
+    #
141
+    #     myprog v1.0.7                         # all clear
142
+    #     myprog v1.0.7-alpha                   # PRERELEASE="alpha"
143
+    #     myprog v1.0.7-alpha+g1aef811.master   # ^^ + some commits after
144
+    #     myprog v1.0.7-alpha+gf14fc4f.api2     # ^^ + on a feature branch
145
+    #     myprog v1.0.7-alpha+gf14fc4f.api2.dirty  # ^^ + tree edited
146
+    #     myprog v1.0.7-alpha+dirty             # tag OK but tree edited
147
+    #     myprog v1.0.7+dirty                   # ^^ but no pre-release id
148
+    #
149
+    # Note that versions with "dirty" should be perceived as kind of
150
+    # dangerous outside developer's own machine.  Versions with sha1 are
151
+    # safer but must not be released.
152
+    #
153
+    # I have considered decorating the git commit refs to make them
154
+    # sort of sortable (e.g. "r1.g1aef811"), but on second thought,
155
+    # I don't think it's good idea to give *any* semantics to meta-data
156
+    # at all.  First, there is no rule that r1<r2<r3; a commit can be
157
+    # removing what other just added and one change can be split to
158
+    # multiple commits.  Also, the whole thing breaks anyway once you
159
+    # rebase your branch (no, it's not a sin).  The sole purpose of
160
+    # meta-data is to *identify* the code, and provide safe path back
161
+    # to tree; commit refs are already perfect for that.
162
+    #
163
+    # FIXME:  Using PRERELEASE for release IDs may not be compatible with
164
+    #         release strategy implemented in release.sh
165
+    #
166
+    local version=$VERSION
167
+    local prerl=$PRERELEASE
168
+    grep ":" <<<"$prerl" && warn "colon in PRERELEASE may corrupt version data: $prerl"
169
+    if git rev-parse HEAD >&/dev/null;
170
+    then    # we are in git repo... so we can get smart
171
+        local lasttag=$(git tag | grep ^v | sort -V | tail -n1)
172
+        if ! git describe --tags --exact-match HEAD >&/dev/null;
173
+        then    # we are at a later commit than the last tag
174
+            local sha=g$(git log -1 --pretty=format:%h HEAD)
175
+            local curbranch=$(git rev-parse --abbrev-ref HEAD)
176
+            local commit="$curbranch.$sha"
177
+        fi
178
+        if test "$(git diff --shortstat 2>/dev/null)" != "";
179
+        then    # the tree is "dirty", i.e. has been edited
180
+            local dirty=dirty
181
+        fi
182
+        test -n "$lasttag" && version=${lasttag:1}
183
+        local suffix=""
184
+        case "$commit:$dirty" in
185
+            :)       suffix=""                ;;
186
+            :dirty)  suffix="+$dirty"         ;;
187
+            *:)      suffix="+$commit"        ;;
188
+            *:dirty) suffix="+$commit.$dirty" ;;
189
+        esac
190
+        test -b "$prerl" && suffix="-$prerl$suffix"
191
+        version="$version$suffix"
192
+    fi
193
+    echo "$version"
194
+}

+ 103
- 0
src/include/deploy.sh View File

@@ -0,0 +1,103 @@
1
+#!/bin/bash
2
+
3
+
4
+check_env() {
5
+    #
6
+    # Check that environment variables have been set properly
7
+    #
8
+    PREFIX="$(readlink -m "$PREFIX")"
9
+    test -d "$PREFIX" || die "PREFIX points to non-existent directory: $PREFIX"
10
+}
11
+
12
+deploy_item() {
13
+    #
14
+    # Deploy item and make it look like wanted
15
+    #
16
+    # usage: deploy_item src dst [mode]
17
+    #
18
+    # Both src and dst must be names of actual items[1],
19
+    # whereas dst must not exist.  On update, dst is
20
+    # usually to be replaced but that is uninstall's
21
+    # job!
22
+    #
23
+    #  [1] Ie. src=foo and dst=/foo/bar does *always*
24
+    #      mean that foo will become 'bar'.  This is
25
+    #      different than traditional `cp` behavior,
26
+    #      when this depends if 'bar' already exists
27
+    #      as a directory.
28
+    #
29
+    # If mode is omitted or empty, MKIT_DEFAULT_MODE is
30
+    # used instead.
31
+    #
32
+    # Directories are copied recursively, and mode is
33
+    # applied only to files.
34
+    #
35
+    local src="$1"
36
+    local dst="$2"
37
+    local mode="${3:-$MKIT_DEFAULT_MODE}"
38
+    if test -d "$src";
39
+    then
40
+        cp -Tvr "$src" "$dst"
41
+        find "$dst" -type f -print0 | xargs -0 chmod -c "$mode"
42
+    else
43
+        command -p install -DTvm "$mode" "$src" "$dst"
44
+    fi
45
+}
46
+
47
+get_dst() {
48
+    #
49
+    # Find out target path for src file $2 of group $1
50
+    #
51
+    local grp=$1
52
+    local src=$2
53
+    echo "$(get_root "$grp")/$(ini 1value "files:$grp:$src")"
54
+}
55
+
56
+get_root() {
57
+    #
58
+    # Find out target rooot for group $1
59
+    #
60
+    local grp="$1"
61
+    local root=$(ini 1value "roots:$grp")
62
+    test -n "$root" || die "missing in config.ini: roots:$grp"
63
+    echo "$root"
64
+}
65
+
66
+install() {
67
+    #
68
+    # Install product
69
+    #
70
+    check_env
71
+    local dst group mode src
72
+    ini values "lists:group" \
73
+      | while read group;
74
+        do
75
+            mode=$(ini 1value "modes:$group")
76
+            ini lskeys "files:$group" \
77
+              | while read src;
78
+                do
79
+                    dst=$(get_dst "$group" "$src")
80
+                    deploy_item "$src" "$dst" "$mode"
81
+                done
82
+        done
83
+    test -f "$MKIT_LOCAL/autoclean" && clean
84
+    true
85
+}
86
+
87
+uninstall() {
88
+    #
89
+    # Uninstall product
90
+    #
91
+    check_env
92
+    local dst group src
93
+    ini values "lists:group" \
94
+      | while read group;
95
+        do
96
+            ini lskeys "files:$group" \
97
+              | while read src;
98
+                do
99
+                    dst=$(get_dst "$group" "$src")
100
+                    rm -vrf "$dst"
101
+                done
102
+        done
103
+}

+ 122
- 0
src/include/ini.sh View File

@@ -0,0 +1,122 @@
1
+#!/bin/bash
2
+
3
+ini() {
4
+    #
5
+    # do ini operation
6
+    #
7
+    local op=$1
8
+    local arg=$2
9
+    local fn
10
+    local limit=_ini_cat
11
+    case $op in
12
+        lskeys) fn=_ini_lskeys   ;;
13
+        sec)    fn=_ini_grepsec  ;;
14
+        values) fn=_ini_greppath ;;
15
+        1value) fn=_ini_greppath; limit="tail -1" ;;
16
+        *)      die "incorrect use of \`ini()\`"
17
+    esac
18
+    <"$MKIT_INI" $fn "$arg" | $limit
19
+}
20
+
21
+_ini_cat() {
22
+    #
23
+    # A no-op for text stream
24
+    #
25
+    while read line;
26
+    do
27
+        printf -- "%s\n" "$line"
28
+    done
29
+}
30
+
31
+_ini_expand() {
32
+    #
33
+    # Expand reference value (prefix only)
34
+    #
35
+    local line suffix ref value
36
+    while read line;                        # [foo:bar]/path
37
+    do
38
+        suffix="${line#\[*\]}"              # /path
39
+        ref="${line%$suffix}"               # [foo:bar]
40
+        ref="${ref%\]}"                     # [foo:bar
41
+        ref="${ref#\[}"                     # foo:bar
42
+        value="$(ini 1value "$ref")"        # foo_bar_value
43
+        printf -- "%s\n" "$value$suffix"    # foo_bar_value/path
44
+    done
45
+}
46
+
47
+_ini_grepkey() {
48
+    #
49
+    # Read key from a section
50
+    #
51
+    local wnt=$1
52
+    grep '.' \
53
+      | grep -v '\s*#' \
54
+      | sed -e 's/ *= */=/; s/ +$//; s/^//;' \
55
+      | grep -e "^$wnt=" \
56
+      | cut -d= -f2- \
57
+      | _ini_maybe_expand
58
+}
59
+
60
+_ini_greppath() {
61
+    #
62
+    # Read key from the right section
63
+    #
64
+    # E.g. `files:share:my/lib.sh` should read
65
+    #
66
+    #     [files:share]
67
+    #         my/lib.sh   = proj/my/lib.sh
68
+    #
69
+    local wnt="$1"
70
+    local wntkey="${wnt##*:}"
71
+    local wntsec="${wnt%:$wntkey}"
72
+    if test "$wntsec" = 'ENV';
73
+    then
74
+        local override="${!wntkey}"
75
+        if test -n "$override";
76
+        then
77
+            echo "$override"
78
+            return
79
+        fi
80
+    fi
81
+    _ini_grepsec "$wntsec" | _ini_grepkey "$wntkey"
82
+}
83
+
84
+_ini_grepsec() {
85
+    #
86
+    # Read one INI section
87
+    #
88
+    local wnt="$1"
89
+    local ok=false
90
+    grep '.' \
91
+      | grep -v '\s*#' \
92
+      | while read line;
93
+        do
94
+            case "$line" in
95
+                \[$wnt\]) ok=true;  continue ;;
96
+                \[*\])    ok=false; continue ;;
97
+            esac
98
+            $ok || continue
99
+            printf -- "%s\n" "$line"
100
+        done \
101
+      | sed -e 's/ *= */=/; s/ +$//; s/^//;'
102
+}
103
+
104
+_ini_lskeys() {
105
+    #
106
+    # List keys from a section
107
+    #
108
+    local sct="$1"
109
+    _ini_grepsec "$sct" | cut -d= -f1 | sort | uniq
110
+}
111
+
112
+_ini_maybe_expand() {
113
+    #
114
+    # Decide whether or not to expand
115
+    #
116
+    if test "$MKIT_INI_EXPAND" -gt 0;
117
+    then
118
+        MKIT_INI_EXPAND=$(( --MKIT_INI_EXPAND )) _ini_expand
119
+    else
120
+        _ini_cat
121
+    fi
122
+}

+ 55
- 0
src/include/mkit.sh View File

@@ -0,0 +1,55 @@
1
+#!/bin/bash
2
+
3
+. "$MKIT_DIR/include/build.sh"  || die "cannot import build.sh"
4
+. "$MKIT_DIR/include/deploy.sh" || die "cannot import deploy.sh"
5
+. "$MKIT_DIR/include/release.sh" || die "cannot import release.sh"
6
+. "$MKIT_DIR/include/ini.sh"    || die "cannot import ini.sh"
7
+
8
+MKIT_INI=${MKIT_INI:-mkit.ini}
9
+MKIT_INI_EXPAND=2
10
+MKIT_PKGNAME=$(ini 1value "ENV:PKGNAME")
11
+MKIT_PROJNAME=$(ini 1value "ENV:PROJNAME")
12
+MKIT_DEFAULT_MODE="644"
13
+
14
+mkit_init() {
15
+    #
16
+    # Do basic initialization
17
+    #
18
+    # Check for ini file, load variables from config.mk
19
+    #
20
+    test -f "$MKIT_INI" || die "cannot find mkit.ini: $MKIT_INI"
21
+    tmp=$(mktemp)
22
+    sed -e 's/ = /=/' < config.mk > "$tmp"
23
+    . "$tmp"
24
+    rm -f "$tmp"
25
+    test -n "$(tr -d '[:space:]' <<<"$MKIT_LOCAL")" \
26
+     || die "MKIT_LOCAL must be non-blank: '$MKIT_LOCAL'"
27
+}
28
+
29
+die() {
30
+    #
31
+    # Exit with message and non-zero exit status
32
+    #
33
+    echo "fatal: $*" >&2
34
+    exit 4
35
+}
36
+
37
+warn() {
38
+    #
39
+    # Print warning message
40
+    #
41
+    echo "$@" >&2
42
+}
43
+
44
+route() {
45
+    #
46
+    # Call correct function based on $1
47
+    #
48
+    case $1 in
49
+        build|build_manpages|clean|dist|install|release_?|uninstall)
50
+            $1
51
+            ;;
52
+        *)
53
+            echo "usage: $(basename "$0") build|clean|dist|install|uninstall" >&2
54
+    esac
55
+}

+ 79
- 0
src/include/release.sh View File

@@ -0,0 +1,79 @@
1
+#!/bin/bash
2
+
3
+__make_ver() {
4
+    local level=$1
5
+    local old=$2
6
+    local oldx=${old%.*.*}
7
+    local oldz=${old#*.*.}
8
+    local tmpy=${old%.*}
9
+    local oldy=${tmpy#*.}
10
+    case $level in
11
+        x) new="$((oldx+1)).0.0"            ;;
12
+        y) new="$oldx.$((oldy+1)).0"        ;;
13
+        z) new="$oldx.$oldy.$((oldz+1))"    ;;
14
+        *) die "invalid release level: $1"  ;;
15
+    esac
16
+    echo "$new"
17
+}
18
+
19
+__release() {
20
+    #
21
+    # Prepare release
22
+    #
23
+    # Span release routines: make a signed tag, check branch
24
+    # and update release branch
25
+    #
26
+    # FIXME: Using PRERELEASE as build.sh:get_version() does may not be
27
+    #        compatible with this release strategy
28
+    #
29
+    local level=$1
30
+    git rev-parse HEAD >&/dev/null || die "cannot release outside git repo"
31
+    local lastfile=$(git diff-tree --no-commit-id --name-only -r HEAD)
32
+    local lasttag=$(git tag | grep ^v | sort -V | tail -n1)
33
+    local changelog="$(git log --oneline "$lasttag..HEAD")"
34
+    local curbranch=$(git rev-parse --abbrev-ref HEAD)
35
+    local dirt="$(git diff --shortstat 2>/dev/null)"
36
+
37
+    local newver=$(__make_ver "$level" "${lasttag#v}")
38
+    local newtag=v$newver
39
+    local higher=$(echo -e "$oldtag\n$newtag" | sort -V | tail -n1)
40
+
41
+    test "$lastfile" = config.mk \
42
+     || die "last change must be version bump in config.mk"
43
+
44
+    test -n "$lasttag" \
45
+     || die "cannot find last tag"
46
+
47
+    test "$newtag" = "$higher" \
48
+     || die "generated tag looks older: $oldtag<$newtag"
49
+
50
+    grep -qw "$newver" config.mk \
51
+     || die "new version not in config.mk: $newver"
52
+
53
+    grep '^....... WIP ' <<<"$changelog" \
54
+     && die "WIP commit since $lasttag"
55
+
56
+    grep -qw "$curbranch" <<<"$RELSRC" \
57
+     || die "you are not on RELSRC branch: $RELSRC"
58
+
59
+    test -z "$dirt" \
60
+     || die "tree is dirty: $dirt"
61
+
62
+    # FIXME: Have user prepare proper message with changelog
63
+
64
+    set -e
65
+    git tag -m "$MKIT_PROJNAME $newtag - $CODENAME" "$newtag"
66
+    git branch -f "$RELDST" "$newtag"
67
+}
68
+
69
+release_x() {
70
+    __release x
71
+}
72
+
73
+release_y() {
74
+    __release y
75
+}
76
+
77
+release_z() {
78
+    __release z
79
+}

+ 16
- 0
src/make View File

@@ -0,0 +1,16 @@
1
+#!/bin/bash
2
+# mkit - simple install helper
3
+# See LICENSE file for copyright and license details.
4
+
5
+die() {
6
+    echo "$@" && exit 9
7
+}
8
+
9
+export MKIT_DIR=${MKIT_DIR:-mkit}
10
+export MKIT_LOCAL=${MKIT_LOCAL:-.mkit}
11
+
12
+. "$MKIT_DIR/include/mkit.sh" || die "failed to init; check if MKIT_DIR is set properly: $MKIT_DIR"
13
+
14
+mkit_init
15
+
16
+route "$@"

+ 41
- 0
src/mkit.mk View File

@@ -0,0 +1,41 @@
1
+# ffoo - Fastfoo - Bash dot on steroids
2
+# See LICENSE file for copyright and license details.
3
+
4
+export MKIT_DIR
5
+
6
+all: options build
7
+
8
+options:
9
+	@echo build options:
10
+	@echo "VERSION  = ${VERSION}"
11
+	@echo "PRERELEASE = ${PRERELEASE}"
12
+	@echo "PREFIX   = ${PREFIX}"
13
+
14
+build:
15
+	@$(MKIT_DIR)/make build
16
+
17
+manpages: build
18
+	@$(MKIT_DIR)/make build_manpages
19
+
20
+clean:
21
+	@$(MKIT_DIR)/make clean
22
+
23
+dist: clean
24
+	@$(MKIT_DIR)/make dist
25
+
26
+install: all
27
+	@$(MKIT_DIR)/make install
28
+
29
+release_x:
30
+	@$(MKIT_DIR)/make release_x
31
+
32
+release_y:
33
+	@$(MKIT_DIR)/make release_y
34
+
35
+release_z:
36
+	@$(MKIT_DIR)/make release_z
37
+
38
+uninstall:
39
+	@$(MKIT_DIR)/make uninstall
40
+
41
+.PHONY: all options clean dist install uninstall release_x release_y release_z

+ 194
- 0
utils/mkit/include/build.sh View File

@@ -0,0 +1,194 @@
1
+#!/bin/bash
2
+
3
+. "$MKIT_DIR/include/ini.sh" || die "cannot import ini.sh"
4
+
5
+
6
+build() {
7
+    #
8
+    # Add meat to all skeletons
9
+    #
10
+    local srcpath
11
+    find src -type f -name '*.skel' \
12
+     | while read srcpath;
13
+       do
14
+           build1 "$srcpath"
15
+       done
16
+}
17
+
18
+build1() {
19
+    #
20
+    # Process one skeleton
21
+    #
22
+    local srcpath dstpath
23
+    srcpath=$1
24
+    dstpath=${srcpath%.skel}
25
+    case $dstpath in
26
+        *.md) <"$srcpath" expand_includes | expand_variables >"$dstpath" ;;
27
+        *)    <"$srcpath"                   expand_variables >"$dstpath" ;;
28
+    esac
29
+    mkdir -p "$MKIT_LOCAL"
30
+    echo "$dstpath" >> "$MKIT_LOCAL/built.lst"
31
+}
32
+
33
+build_manpages() {
34
+    local manfile mdfile
35
+    if command -v ronn >/dev/null;
36
+    then
37
+        ini lskeys "files:man" \
38
+          | while read manfile;
39
+            do
40
+                mdfile="$manfile.md"
41
+                ronn -r "$mdfile"
42
+                mkdir -p "$MKIT_LOCAL"
43
+                echo "$manfile" >> "$MKIT_LOCAL/built.lst"
44
+            done
45
+    else
46
+        echo "ronn is not installed"
47
+        return 1
48
+    fi
49
+}
50
+
51
+clean() {
52
+    #
53
+    # Clean up tree after building
54
+    #
55
+    test -f "$MKIT_LOCAL/built.lst" && {
56
+        <"$MKIT_LOCAL/built.lst" xargs -r rm -f
57
+        rm -f "$MKIT_LOCAL/built.lst"
58
+        rmdir --ignore-fail-on-non-empty "$MKIT_LOCAL"
59
+    }
60
+    true
61
+}
62
+
63
+dist() {
64
+    #
65
+    # Create distributable tarball
66
+    #
67
+    local version=$(get_version)
68
+    local dirname=$MKIT_PKGNAME-$version
69
+    mkdir -p "$dirname"
70
+    local item
71
+    cp -R "$(ini values "lists:dist")" "$dirname"
72
+    sed -i -e "s/^VERSION = .*/VERSION = $version/" "$dirname/config.mk"
73
+    tar -cf "$dirname.tar" "$dirname"
74
+    gzip "$dirname.tar"
75
+    rm -rf "$dirname"
76
+}
77
+
78
+expand_includes() {
79
+    #
80
+    # Expand include directives
81
+    #
82
+    # Expand e.g. `<!-- include4: foo.sh -->` to include code of foo.sh
83
+    #
84
+    perl -we '
85
+        use strict;
86
+        my $text;
87
+        while (<>) {
88
+            chomp;
89
+            if (m/<!-- include4: (\S+) -->/) {
90
+                open my $fh, $1 or warn "cannot find: $1";
91
+                my $text = do { local($/); <$fh> };
92
+                close $fh;
93
+                $text =~ s/^(.)/    $1/gm;
94
+                chomp $text;
95
+                print "$text\n";
96
+            } else {
97
+                print "$_\n";
98
+            }
99
+        }
100
+    '
101
+}
102
+
103
+expand_variables() {
104
+    #
105
+    # Expand variable values
106
+    #
107
+    local script=$(mktemp --tmpdir mkit-tmp.XXXXXXXXXX)
108
+    local varname varvalue
109
+    ini lskeys "vars" \
110
+      | while read varname;
111
+        do
112
+            varvalue="$(ini 1value "vars:$varname" | sed -e 's/\$/\\$/' )"
113
+            echo "s|$varname|$varvalue|;" >> "$script"
114
+        done
115
+    echo "s|__CODENAME__|$CODENAME|;"     >> "$script"
116
+    echo "s|__VERSION__|$(get_version)|;" >> "$script"
117
+    perl -wp "$script"
118
+    rm "$script"
119
+}
120
+
121
+get_version() {
122
+    #
123
+    # Build semver version string with build metadata
124
+    #
125
+    # Build version string from available info using following
126
+    # logic:
127
+    #
128
+    #  1. use VERSION (from config.mk)
129
+    #  2. if we are in git, override the version with last tag
130
+    #  3. if set, add PRERELEASE (from config.mk) as pre-release ID
131
+    #     (afer dash)
132
+    #  4. if we are at a later commit than the last tag, add branch
133
+    #     name and commit sha1 to build metadata (after plus sign)
134
+    #  5. if the tree is "dirty", i.e. has uncommited changes,
135
+    #     add "dirty" to build metadata
136
+    #
137
+    # The version is compatible with SemVer 2.0.0.
138
+    #
139
+    # Examples:
140
+    #
141
+    #     myprog v1.0.7                         # all clear
142
+    #     myprog v1.0.7-alpha                   # PRERELEASE="alpha"
143
+    #     myprog v1.0.7-alpha+g1aef811.master   # ^^ + some commits after
144
+    #     myprog v1.0.7-alpha+gf14fc4f.api2     # ^^ + on a feature branch
145
+    #     myprog v1.0.7-alpha+gf14fc4f.api2.dirty  # ^^ + tree edited
146
+    #     myprog v1.0.7-alpha+dirty             # tag OK but tree edited
147
+    #     myprog v1.0.7+dirty                   # ^^ but no pre-release id
148
+    #
149
+    # Note that versions with "dirty" should be perceived as kind of
150
+    # dangerous outside developer's own machine.  Versions with sha1 are
151
+    # safer but must not be released.
152
+    #
153
+    # I have considered decorating the git commit refs to make them
154
+    # sort of sortable (e.g. "r1.g1aef811"), but on second thought,
155
+    # I don't think it's good idea to give *any* semantics to meta-data
156
+    # at all.  First, there is no rule that r1<r2<r3; a commit can be
157
+    # removing what other just added and one change can be split to
158
+    # multiple commits.  Also, the whole thing breaks anyway once you
159
+    # rebase your branch (no, it's not a sin).  The sole purpose of
160
+    # meta-data is to *identify* the code, and provide safe path back
161
+    # to tree; commit refs are already perfect for that.
162
+    #
163
+    # FIXME:  Using PRERELEASE for release IDs may not be compatible with
164
+    #         release strategy implemented in release.sh
165
+    #
166
+    local version=$VERSION
167
+    local prerl=$PRERELEASE
168
+    grep ":" <<<"$prerl" && warn "colon in PRERELEASE may corrupt version data: $prerl"
169
+    if git rev-parse HEAD >&/dev/null;
170
+    then    # we are in git repo... so we can get smart
171
+        local lasttag=$(git tag | grep ^v | sort -V | tail -n1)
172
+        if ! git describe --tags --exact-match HEAD >&/dev/null;
173
+        then    # we are at a later commit than the last tag
174
+            local sha=g$(git log -1 --pretty=format:%h HEAD)
175
+            local curbranch=$(git rev-parse --abbrev-ref HEAD)
176
+            local commit="$curbranch.$sha"
177
+        fi
178
+        if test "$(git diff --shortstat 2>/dev/null)" != "";
179
+        then    # the tree is "dirty", i.e. has been edited
180
+            local dirty=dirty
181
+        fi
182
+        test -n "$lasttag" && version=${lasttag:1}
183
+        local suffix=""
184
+        case "$commit:$dirty" in
185
+            :)       suffix=""                ;;
186
+            :dirty)  suffix="+$dirty"         ;;
187
+            *:)      suffix="+$commit"        ;;
188
+            *:dirty) suffix="+$commit.$dirty" ;;
189
+        esac
190
+        test -b "$prerl" && suffix="-$prerl$suffix"
191
+        version="$version$suffix"
192
+    fi
193
+    echo "$version"
194
+}

+ 103
- 0
utils/mkit/include/deploy.sh View File

@@ -0,0 +1,103 @@
1
+#!/bin/bash
2
+
3
+
4
+check_env() {
5
+    #
6
+    # Check that environment variables have been set properly
7
+    #
8
+    PREFIX="$(readlink -m "$PREFIX")"
9
+    test -d "$PREFIX" || die "PREFIX points to non-existent directory: $PREFIX"
10
+}
11
+
12
+deploy_item() {
13
+    #
14
+    # Deploy item and make it look like wanted
15
+    #
16
+    # usage: deploy_item src dst [mode]
17
+    #
18
+    # Both src and dst must be names of actual items[1],
19
+    # whereas dst must not exist.  On update, dst is
20
+    # usually to be replaced but that is uninstall's
21
+    # job!
22
+    #
23
+    #  [1] Ie. src=foo and dst=/foo/bar does *always*
24
+    #      mean that foo will become 'bar'.  This is
25
+    #      different than traditional `cp` behavior,
26
+    #      when this depends if 'bar' already exists
27
+    #      as a directory.
28
+    #
29
+    # If mode is omitted or empty, MKIT_DEFAULT_MODE is
30
+    # used instead.
31
+    #
32
+    # Directories are copied recursively, and mode is
33
+    # applied only to files.
34
+    #
35
+    local src="$1"
36
+    local dst="$2"
37
+    local mode="${3:-$MKIT_DEFAULT_MODE}"
38
+    if test -d "$src";
39
+    then
40
+        cp -Tvr "$src" "$dst"
41
+        find "$dst" -type f -print0 | xargs -0 chmod -c "$mode"
42
+    else
43
+        command -p install -DTvm "$mode" "$src" "$dst"
44
+    fi
45
+}
46
+
47
+get_dst() {
48
+    #
49
+    # Find out target path for src file $2 of group $1
50
+    #
51
+    local grp=$1
52
+    local src=$2
53
+    echo "$(get_root "$grp")/$(ini 1value "files:$grp:$src")"
54
+}
55
+
56
+get_root() {
57
+    #
58
+    # Find out target rooot for group $1
59
+    #
60
+    local grp="$1"
61
+    local root=$(ini 1value "roots:$grp")
62
+    test -n "$root" || die "missing in config.ini: roots:$grp"
63
+    echo "$root"
64
+}
65
+
66
+install() {
67
+    #
68
+    # Install product
69
+    #
70
+    check_env
71
+    local dst group mode src
72
+    ini values "lists:group" \
73
+      | while read group;
74
+        do
75
+            mode=$(ini 1value "modes:$group")
76
+            ini lskeys "files:$group" \
77
+              | while read src;
78
+                do
79
+                    dst=$(get_dst "$group" "$src")
80
+                    deploy_item "$src" "$dst" "$mode"
81
+                done
82
+        done
83
+    test -f "$MKIT_LOCAL/autoclean" && clean
84
+    true
85
+}
86
+
87
+uninstall() {
88
+    #
89
+    # Uninstall product
90
+    #
91
+    check_env
92
+    local dst group src
93
+    ini values "lists:group" \
94
+      | while read group;
95
+        do
96
+            ini lskeys "files:$group" \
97
+              | while read src;
98
+                do
99
+                    dst=$(get_dst "$group" "$src")
100
+                    rm -vrf "$dst"
101
+                done
102
+        done
103
+}

+ 122
- 0
utils/mkit/include/ini.sh View File

@@ -0,0 +1,122 @@
1
+#!/bin/bash
2
+
3
+ini() {
4
+    #
5
+    # do ini operation
6
+    #
7
+    local op=$1
8
+    local arg=$2
9
+    local fn
10
+    local limit=_ini_cat
11
+    case $op in
12
+        lskeys) fn=_ini_lskeys   ;;
13
+        sec)    fn=_ini_grepsec  ;;
14
+        values) fn=_ini_greppath ;;
15
+        1value) fn=_ini_greppath; limit="tail -1" ;;
16
+        *)      die "incorrect use of \`ini()\`"
17
+    esac
18
+    <"$MKIT_INI" $fn "$arg" | $limit
19
+}
20
+
21
+_ini_cat() {
22
+    #
23
+    # A no-op for text stream
24
+    #
25
+    while read line;
26
+    do
27
+        printf -- "%s\n" "$line"
28
+    done
29
+}
30
+
31
+_ini_expand() {
32
+    #
33
+    # Expand reference value (prefix only)
34
+    #
35
+    local line suffix ref value
36
+    while read line;                        # [foo:bar]/path
37
+    do
38
+        suffix="${line#\[*\]}"              # /path
39
+        ref="${line%$suffix}"               # [foo:bar]
40
+        ref="${ref%\]}"                     # [foo:bar
41
+        ref="${ref#\[}"                     # foo:bar
42
+        value="$(ini 1value "$ref")"        # foo_bar_value
43
+        printf -- "%s\n" "$value$suffix"    # foo_bar_value/path
44
+    done
45
+}
46
+
47
+_ini_grepkey() {
48
+    #
49
+    # Read key from a section
50
+    #
51
+    local wnt=$1
52
+    grep '.' \
53
+      | grep -v '\s*#' \
54
+      | sed -e 's/ *= */=/; s/ +$//; s/^//;' \
55
+      | grep -e "^$wnt=" \
56
+      | cut -d= -f2- \
57
+      | _ini_maybe_expand
58
+}
59
+
60
+_ini_greppath() {
61
+    #
62
+    # Read key from the right section
63
+    #
64
+    # E.g. `files:share:my/lib.sh` should read
65
+    #
66
+    #     [files:share]
67
+    #         my/lib.sh   = proj/my/lib.sh
68
+    #
69
+    local wnt="$1"
70
+    local wntkey="${wnt##*:}"
71
+    local wntsec="${wnt%:$wntkey}"
72
+    if test "$wntsec" = 'ENV';
73
+    then
74
+        local override="${!wntkey}"
75
+        if test -n "$override";
76
+        then
77
+            echo "$override"
78
+            return
79
+        fi
80
+    fi
81
+    _ini_grepsec "$wntsec" | _ini_grepkey "$wntkey"
82
+}
83
+
84
+_ini_grepsec() {
85
+    #
86
+    # Read one INI section
87
+    #
88
+    local wnt="$1"
89
+    local ok=false
90
+    grep '.' \
91
+      | grep -v '\s*#' \
92
+      | while read line;
93
+        do
94
+            case "$line" in
95
+                \[$wnt\]) ok=true;  continue ;;
96
+                \[*\])    ok=false; continue ;;
97
+            esac
98
+            $ok || continue
99
+            printf -- "%s\n" "$line"
100
+        done \
101
+      | sed -e 's/ *= */=/; s/ +$//; s/^//;'
102
+}
103
+
104
+_ini_lskeys() {
105
+    #
106
+    # List keys from a section
107
+    #
108
+    local sct="$1"
109
+    _ini_grepsec "$sct" | cut -d= -f1 | sort | uniq
110
+}
111
+
112
+_ini_maybe_expand() {
113
+    #
114
+    # Decide whether or not to expand
115
+    #
116
+    if test "$MKIT_INI_EXPAND" -gt 0;
117
+    then
118
+        MKIT_INI_EXPAND=$(( --MKIT_INI_EXPAND )) _ini_expand
119
+    else
120
+        _ini_cat
121
+    fi
122
+}

+ 55
- 0
utils/mkit/include/mkit.sh View File

@@ -0,0 +1,55 @@
1
+#!/bin/bash
2
+
3
+. "$MKIT_DIR/include/build.sh"  || die "cannot import build.sh"
4
+. "$MKIT_DIR/include/deploy.sh" || die "cannot import deploy.sh"
5
+. "$MKIT_DIR/include/release.sh" || die "cannot import release.sh"
6
+. "$MKIT_DIR/include/ini.sh"    || die "cannot import ini.sh"
7
+
8
+MKIT_INI=${MKIT_INI:-mkit.ini}
9
+MKIT_INI_EXPAND=2
10
+MKIT_PKGNAME=$(ini 1value "ENV:PKGNAME")
11
+MKIT_PROJNAME=$(ini 1value "ENV:PROJNAME")
12
+MKIT_DEFAULT_MODE="644"
13
+
14
+mkit_init() {
15
+    #
16
+    # Do basic initialization
17
+    #
18
+    # Check for ini file, load variables from config.mk
19
+    #
20
+    test -f "$MKIT_INI" || die "cannot find mkit.ini: $MKIT_INI"
21
+    tmp=$(mktemp)
22
+    sed -e 's/ = /=/' < config.mk > "$tmp"
23
+    . "$tmp"
24
+    rm -f "$tmp"
25
+    test -n "$(tr -d '[:space:]' <<<"$MKIT_LOCAL")" \
26
+     || die "MKIT_LOCAL must be non-blank: '$MKIT_LOCAL'"
27
+}
28
+
29
+die() {
30
+    #
31
+    # Exit with message and non-zero exit status
32
+    #
33
+    echo "fatal: $*" >&2
34
+    exit 4
35
+}
36
+
37
+warn() {
38
+    #
39
+    # Print warning message
40
+    #
41
+    echo "$@" >&2
42
+}
43
+
44
+route() {
45
+    #
46
+    # Call correct function based on $1
47
+    #
48
+    case $1 in
49
+        build|build_manpages|clean|dist|install|release_?|uninstall)
50
+            $1
51
+            ;;
52
+        *)
53
+            echo "usage: $(basename "$0") build|clean|dist|install|uninstall" >&2
54
+    esac
55
+}

+ 79
- 0
utils/mkit/include/release.sh View File

@@ -0,0 +1,79 @@
1
+#!/bin/bash
2
+
3
+__make_ver() {
4
+    local level=$1
5
+    local old=$2
6
+    local oldx=${old%.*.*}
7
+    local oldz=${old#*.*.}
8
+    local tmpy=${old%.*}
9
+    local oldy=${tmpy#*.}
10
+    case $level in
11
+        x) new="$((oldx+1)).0.0"            ;;
12
+        y) new="$oldx.$((oldy+1)).0"        ;;
13
+        z) new="$oldx.$oldy.$((oldz+1))"    ;;
14
+        *) die "invalid release level: $1"  ;;
15
+    esac
16
+    echo "$new"
17
+}
18
+
19
+__release() {
20
+    #
21
+    # Prepare release
22
+    #
23
+    # Span release routines: make a signed tag, check branch
24
+    # and update release branch
25
+    #
26
+    # FIXME: Using PRERELEASE as build.sh:get_version() does may not be
27
+    #        compatible with this release strategy
28
+    #
29
+    local level=$1
30
+    git rev-parse HEAD >&/dev/null || die "cannot release outside git repo"
31
+    local lastfile=$(git diff-tree --no-commit-id --name-only -r HEAD)
32
+    local lasttag=$(git tag | grep ^v | sort -V | tail -n1)
33
+    local changelog="$(git log --oneline "$lasttag..HEAD")"
34
+    local curbranch=$(git rev-parse --abbrev-ref HEAD)
35
+    local dirt="$(git diff --shortstat 2>/dev/null)"
36
+
37
+    local newver=$(__make_ver "$level" "${lasttag#v}")
38
+    local newtag=v$newver
39
+    local higher=$(echo -e "$oldtag\n$newtag" | sort -V | tail -n1)
40
+
41
+    test "$lastfile" = config.mk \
42
+     || die "last change must be version bump in config.mk"
43
+
44
+    test -n "$lasttag" \
45
+     || die "cannot find last tag"
46
+
47
+    test "$newtag" = "$higher" \
48
+     || die "generated tag looks older: $oldtag<$newtag"
49
+
50
+    grep -qw "$newver" config.mk \
51
+     || die "new version not in config.mk: $newver"
52
+
53
+    grep '^....... WIP ' <<<"$changelog" \
54
+     && die "WIP commit since $lasttag"
55
+
56
+    grep -qw "$curbranch" <<<"$RELSRC" \
57
+     || die "you are not on RELSRC branch: $RELSRC"
58
+
59
+    test -z "$dirt" \
60
+     || die "tree is dirty: $dirt"
61
+
62
+    # FIXME: Have user prepare proper message with changelog
63
+
64
+    set -e
65
+    git tag -m "$MKIT_PROJNAME $newtag - $CODENAME" "$newtag"
66
+    git branch -f "$RELDST" "$newtag"
67
+}
68
+
69
+release_x() {
70
+    __release x
71
+}
72
+
73
+release_y() {
74
+    __release y
75
+}
76
+
77
+release_z() {
78
+    __release z
79
+}

+ 16
- 0
utils/mkit/make View File

@@ -0,0 +1,16 @@
1
+#!/bin/bash
2
+# mkit - simple install helper
3
+# See LICENSE file for copyright and license details.
4
+
5
+die() {
6
+    echo "$@" && exit 9
7
+}
8
+
9
+export MKIT_DIR=${MKIT_DIR:-mkit}
10
+export MKIT_LOCAL=${MKIT_LOCAL:-.mkit}
11
+
12
+. "$MKIT_DIR/include/mkit.sh" || die "failed to init; check if MKIT_DIR is set properly: $MKIT_DIR"
13
+
14
+mkit_init
15
+
16
+route "$@"

+ 41
- 0
utils/mkit/mkit.mk View File

@@ -0,0 +1,41 @@
1
+# ffoo - Fastfoo - Bash dot on steroids
2
+# See LICENSE file for copyright and license details.
3
+
4
+export MKIT_DIR
5
+
6
+all: options build
7
+
8
+options:
9
+	@echo build options:
10
+	@echo "VERSION  = ${VERSION}"
11
+	@echo "PRERELEASE = ${PRERELEASE}"
12
+	@echo "PREFIX   = ${PREFIX}"
13
+
14
+build:
15
+	@$(MKIT_DIR)/make build
16
+
17
+manpages: build
18
+	@$(MKIT_DIR)/make build_manpages
19
+
20
+clean:
21
+	@$(MKIT_DIR)/make clean
22
+
23
+dist: clean
24
+	@$(MKIT_DIR)/make dist
25
+
26
+install: all
27
+	@$(MKIT_DIR)/make install
28
+
29
+release_x:
30
+	@$(MKIT_DIR)/make release_x
31
+
32
+release_y:
33
+	@$(MKIT_DIR)/make release_y
34
+
35
+release_z:
36
+	@$(MKIT_DIR)/make release_z
37
+
38
+uninstall:
39
+	@$(MKIT_DIR)/make uninstall
40
+
41
+.PHONY: all options clean dist install uninstall release_x release_y release_z

+ 15
- 0
utils/tfkit/README.md View File

@@ -0,0 +1,15 @@
1
+TFKIT
2
+=====
3
+
4
+A simple portable (or rather "movable") test framework.
5
+
6
+ 1. Copy (or `git submodule`) *test* sub-folder to your place
7
+    where you want to run tests.
8
+
9
+ 2. Run your framework by `./tfkit/runtests`.
10
+
11
+ 3. Don't be surprised--you haven't added any tests yet :)
12
+
13
+ 4. Read tfkit/README.testing.md
14
+
15
+ 5. Start adding tests to `tests` folder

+ 360
- 0
utils/tfkit/doc/README.md View File

@@ -0,0 +1,360 @@
1
+Tests
2
+=====
3
+
4
+Running tests is handled by tfkit/runtests:
5
+
6
+    $ tfkit/runtest [filter]
7
+
8
+*filter* is a regular expression to be applied to sub-test name, running
9
+only the matching ones.  See below for details.
10
+
11
+
12
+Writing tests
13
+-------------
14
+
15
+Tests can be written in any scripting language, although the built-in
16
+framework, written in Bash, provides some useful features for writing
17
+certain kind of relatively simple tests.
18
+
19
+The harness, though, assumes that:
20
+
21
+ *  Any direct sub-directory of `$TF_SUITE` directory ("tests" by default)
22
+    that contains at least *TF_RUN* executable becomes a test,
23
+
24
+ *  basename of this directory becomes the name of the test,
25
+
26
+ *  and return code from running the executable is reported
27
+    as result of the test, according to "Exit status" chapter below.
28
+
29
+
30
+Naming
31
+------
32
+
33
+Test name should start with name of the module that is tested and
34
+underscore.  If module name contains dots, they should be replaced with
35
+underscores as well.
36
+
37
+    core_sanity
38
+    mod_submod_function
39
+    ini_iniread
40
+
41
+are valid test names.
42
+
43
+
44
+Data
45
+----
46
+
47
+Should the test need any data, just leave it around in the test directory
48
+along with *TF_RUN*.
49
+
50
+Note that before running, the whole test directory is automatically
51
+copied to a temporary location (one per test), and should the test fail,
52
+copied back as a debugging artifact.  For this reason, *do not store
53
+huge amounts of data here*.  If you really need huge data, consider
54
+obtaining it (and throwing it away) within runtime of *TF_RUN*.
55
+
56
+
57
+Exit status
58
+-----------
59
+
60
+We try hard to follow this semantic:
61
+
62
+ *  *Zero* means *OK* -- test has been run and passed.
63
+
64
+ *  *One* means *Failure* -- test has been run but failed (e.g. found
65
+     a bug).
66
+
67
+ *  *Two* means *Bailout* --  test has decided not to run at all.
68
+
69
+ *  *Three* means *Error* -- there was error detected during execution,
70
+     but script was able to clean up properly.
71
+
72
+ *  *Four* means *Panic* -- there was other error but script *was not*
73
+     able to clean up properly.
74
+
75
+ *  Anything else should indicate other uncaught errors, including those
76
+    outside control of the program such as segfaults in the test code
77
+    or test being SIGKILLed.
78
+
79
+Notice that the higher the value is, the worse situation it indicates.
80
+Thus, if a test is composed of several sub-tests, you need to make sure
81
+to always **exit with the highest value** (subtest.sh does take care
82
+of this).
83
+
84
+See *common.sh* for functions and variables to help with handling exit
85
+statuses with this semantic.
86
+
87
+Also see Notes section for more details on exit statuses, including
88
+cheat sheet and dscussuion.
89
+
90
+
91
+Framework
92
+---------
93
+
94
+
95
+### harness.sh ###
96
+
97
+This part is not intended to be used in tests, but rather contains
98
+functions that help govern test discovery, preparation and execution as
99
+is described in previous chapters.  Feel free to poke around, of course.
100
+
101
+
102
+### subtest.sh ###
103
+
104
+As name suggests, this file defines few functions to handle subtests
105
+in *TF_RUN*.
106
+
107
+In order to make use of the subtests functionality, you will need to
108
+define two functions yourself:  `tf_enum_subtests` to enumerate names of
109
+tests you want to run, and `tf_name2cmd` to translate each name an actual
110
+command that would perform it and return with the the correct exit status.
111
+
112
+The minimal *TF_RUN* with two subtests could look like this:
113
+
114
+    #!/bin/bash
115
+
116
+    . $TF_DIR/include/subtest.sh
117
+
118
+    tf_enum_subtests() {
119
+        echo test1
120
+        echo test2
121
+        something && echo test3
122
+    }
123
+
124
+    tf_name2cmd() {
125
+        case $1 in
126
+            test1)  echo myprog foo ;;
127
+            test2)  echo myprog bar ;;
128
+        esac
129
+    }
130
+
131
+    tf_do_subtests
132
+
133
+At the end, `tf_do_subtests` acts as a launcher of the actual test.
134
+In short, it will
135
+
136
+ *  take each enumerated subtest from `tf_enum_subtests`,
137
+ *  source *TF_SETUP*, if such file is found,
138
+ *  translate te subtest name to a command,
139
+ *  launch the command,
140
+ *  source *TF_CLEANUP*, if such file is found,
141
+ *  and report "worst" exit status encountered.
142
+
143
+All but the first and last step is done by `tf_do_subtest`, so in some
144
+cases you may want to re-define this one as well.
145
+
146
+Note that subtest names need to be single words (`[a-zA-Z0-9_]`).
147
+
148
+
149
+### tools.sh ###
150
+
151
+This file contains various tools and utilities to help with testing.
152
+
153
+Curently there is only one function, `tf_testflt` designed to help write
154
+tests for simple unix filters.
155
+
156
+
157
+#### tf_testflt ####
158
+
159
+The idea is that tester specifies
160
+
161
+ *  test name,
162
+ *  command to launch the system under test,
163
+ *  a data stream to use as STDIN,
164
+ *  and expected STDOUT, STDERR, and exit status.
165
+
166
+and tf_testflt launches the command, collects tha data and evaluates
167
+and reports the result using unified diff.
168
+
169
+In its simplest form:
170
+
171
+    tf_testflt -n foo my_command arg
172
+
173
+the function will run `my_command arg` (not piping anything to it),
174
+and will expect it to finish with exit status 0 and empty both STDERR
175
+and STDOUT.
176
+
177
+Example of full form,
178
+
179
+    tf_testflt -n foo -i foo.in -O foo.stdout -E foo.stderr -S 2 myprog
180
+
181
+will pipe foo.in into `myprog`, expecting exit status of 2, and STDOUT and
182
+STDERR as above.  Notice that parameters specifying expected values are
183
+uppercase, and those specifying input values are lowercase.
184
+
185
+Specifying name is mandatory, because it's used in reporting messages,
186
+and as a basis for naming temporary result files: these are saved in
187
+*results* subdirectory and kept for further reference.
188
+
189
+
190
+### common.sh ###
191
+
192
+This includes simple functions and variables shared between both mentioned
193
+libraries.
194
+
195
+First group is designed to help support the exit status semantic:
196
+
197
+ *  The functions are `tf_exit_pass`, `tf_exit_fail`, `tf_exit_bailout`,
198
+    `tf_exit_error` and `tf_exit_panic` and each take any number of
199
+    parameters that are printed on stderr.
200
+
201
+ *  The variables are `TF_ES_OK`, `TF_ES_FAIL`, `TF_ES_BAILOUT`,
202
+    `TF_ES_ERROR` and `TF_ES_PANIC` and are supposed to be used with
203
+    `return` builtin, e.g. to return from `tf_exit_error`.
204
+
205
+Second group is useful to better control output:  functions `tf_warn`,
206
+`tf_debug` and `tf_think` are used to print stuff on STDERR.  Use of
207
+`tf_warn` is apparent, just as `tf_debug`, the latter being muted if
208
+`TF_DEBUG` is set to `false` (set it to `true` to turn on debugging).
209
+
210
+`tf_think` is used for progress info, and is muted unless `TF_VERBOSE`
211
+is set to `true`, which is by default.
212
+
213
+
214
+### Setup and cleanup ###
215
+
216
+Special files *TF_SETUP* and *TF_CLEANUP* (one of them or both) can be
217
+added along with *TF_RUN*.  These will be sourced before (*TF_SETUP*)
218
+and after every subtest (*TF_CLEANUP*).
219
+
220
+First, if any of these files are missing, it is considered as if the
221
+respective phase succeeded.  Second, if setup phase fails, test will
222
+be skipped and subtest exit status will be *TF_ES_BAILOUT*.   Last,
223
+if cleanup fails (no matter result of setup), subtests aborts with
224
+*TF_ES_PANIC* returned.  Be aware that in this case the actual test
225
+status, albeit useful, is lost.
226
+
227
+When coming from other test frameworks, this may feel harsh, but note
228
+that this has been designed with the idea that if a cleanup fails,
229
+it may render all further tests are automatically unsafe, because the
230
+environment is not as expected.
231
+
232
+To cope with this behavior, try to bear in mind following advice:
233
+
234
+ 1. Make sure you write setup/cleanup procedures with extreme care and
235
+    test them well.
236
+
237
+ 2. Do not do complicated and risky things in the setup/cleanup phases.
238
+
239
+ 3. If you need to do such things, consider doing them in the *TF_RUN*
240
+    instead of doing them for all subtests.
241
+
242
+ 4. You don't need to clean up everything, the contents of the testing dir
243
+    will be moved out from the test system.
244
+
245
+ 5. If there are scenarios you can safely fix or ignore, handle them in
246
+    a robust manner.
247
+
248
+
249
+Notes
250
+-----
251
+
252
+
253
+### bailout vs. `tf_enum_subtests` ###
254
+
255
+One more note to claify relation of bailout and `tf_enum_subtests`.
256
+As you may have noticed, there are two ways how to skip a test:
257
+return prematurely with `TF_ES_BAILOUT`, or suppress enumeration in
258
+`tf_enum_subtests`.  The problem is that the latter does not do anything
259
+to inform upper in the stack that a test has been skipped, which seems to
260
+break the principle described in the previous chapters.
261
+
262
+Don't confuse these mechanisms, though. Each is supposed to be used
263
+for distinct purpose.  Compare: by using the `tf_enum_subtests` you are
264
+saying that you actually **did not even want** to run the test in the
265
+first place.  By using `TF_ES_BAILOUT`, you are saying that you **wanted**
266
+to run the test but could not.
267
+
268
+A few common cases if that helps you:
269
+
270
+ *  If during the test you find out that for some reason it can't be
271
+    carried out (e.g. an external resource is not available, or
272
+    something outside the SUT is broken), use `TF_ES_BAILOUT`.
273
+
274
+ *  If you want to disable the test because for some long-term condition,
275
+    e.g. a known bug outside SUT but preventing execution of the test
276
+    is not fixed, use `tf_enum_subtests`.
277
+
278
+ *  If you want to filter out some sub-tests to only for some platforms,
279
+    e.g. 64-bit architecture, (IOW, you can safely check that a
280
+    sub-test would be totally pointless if run on this box), use
281
+    `tf_enum_subtests`.
282
+
283
+ *  If you want to disable (comment out test) that you might not have
284
+    implemented yet or is broken (and for some reason you still want
285
+    it to haunt the test code), use `tf_enum_subtests` and properly
286
+    comment the reasons in code.
287
+
288
+ *  If in doubt, use `TF_ES_BAILOUT`.
289
+
290
+
291
+### On exit statuses: three and above ###
292
+
293
+The difference in *error*, *panic* and higher values is subtle but
294
+important.  Follow me as I try to explain:
295
+
296
+ 1. If script has changed something on the system outside the working
297
+    directory, it is apparently expected to revert that change.
298
+
299
+ 2. Now if an error occurs, but the code responsible for cleaning up is
300
+    safely run, you can say there was *error but we have recovered*.
301
+
302
+ 3. But if the change can't be reverted safely, we know that we have
303
+    broken something and latter code may lead to weird results (including
304
+    masking bugs(!)), it's time to *panic* (in the code, not in real
305
+    life ;))
306
+
307
+ 4. And then there are corner cases like a bug in the script, OOM kill
308
+    or timeout when the status will be different and not really controlled
309
+    by the script.  Such cases will have to be treated the same way as
310
+    the "panic" case, but...
311
+
312
+ 5. the use of *panic* adds hint that the status has been set consciously
313
+    by the script, albeit exiting "in a hurry"--without proper clean up.
314
+
315
+Unfortunately there will be cases like above but with the error code less
316
+than four.   Example is a bash script syntax error, which returns 2, or
317
+Python exception which returns 1.  Yes, in such cases the information
318
+conveyed by the exit status is wrong and you should do everything to
319
+avoid it.
320
+
321
+Possibilities like "test has passed but then something blew up" exist,
322
+but conveying this information is responsibility of the test output.
323
+
324
+Following table can be used as a cheat-sheet:
325
+
326
+    .---------------------------------------------------------------.
327
+    | e |    state of         |                                     |
328
+    | s |---------------------| script says                         |
329
+    |   | SUT   | environment |                                     |
330
+    |---|-------|-------------|-------------------------------------|
331
+    | 0 | OK    | safe        | test passed, everything worked fine |
332
+    | 1 | buggy | safe        | test failed, everything worked fine |
333
+    | 2 | ???   | safe        | I decided not to run the test       |
334
+    | 3 | ???   | safe        | Something blew up but I managed to  |
335
+    |   |       |             | clean up (I promise!)               |
336
+    | 4 | ???   | broken      | Something blew up and I rushed out  |
337
+    |   |       |             | in panic                            |
338
+    | * | ???   | broken      | ...nothing (is dead)                |
339
+    '---------------------------------------------------------------'
340
+
341
+As you can see, following this semantic allows us to see both the state
342
+of the system under test (SUT) *and* the environment.
343
+
344
+Following table illustrates how different statuses map to different
345
+scenarios with regard to test result as well as state of the environment:
346
+
347
+    .--------------------------------------------------.
348
+    | environment |  test result   |  test result      |
349
+    |             | pass fail unkn | pass fail unkn    |
350
+    |-------------|----------------|-------------------|
351
+    | clean(ed)   |  0    1    3   |  OK  FAIL ERROR   |
352
+    | untouched   |  ~    ~    2   |  ~    ~   BAILOUT |
353
+    | mess        |  ~    ~    4   |  ~    ~   PANIC   |
354
+    | ?! (trap)   |  ~    ~    5   |  ~    ~   ~       |
355
+    | ?! (sig 9)  |  ~    ~    137 |  ~    ~   ~       |
356
+    | ?! (aliens) |  ~    ~    ?   |  ~    ~   ~       |
357
+    '-------------|----------------|-------------------|
358
+                  |  exit status   |  human-readable   |
359
+                  |                |  name (TF_ES_*)   |
360
+                  '------------------------------------'

+ 26
- 0
utils/tfkit/doc/templates/grep_engine/TF_RUN View File

@@ -0,0 +1,26 @@
1
+#!/bin/bash
2
+
3
+. "$TF_DIR/include/subtest.sh"
4
+. "$TF_DIR/include/tools.sh"
5
+
6
+tf_enum_subtests() {
7
+    echo fixed
8
+    echo basic
9
+    echo extended
10
+#   echo perl       # TODO: write test
11
+}
12
+
13
+tf_name2cmd() {
14
+    local name=$1
15
+    local t_in="test/ALL.stdin"
16
+    local o_out="oracle/$name.stdout"
17
+    local args
18
+    case $name in
19
+        fixed)       args="-F 'The mask *.* matches all.'" ;;
20
+        basic)       args="-G 'he.*'" ;;
21
+        extended)    args="-P '.*og?g'" ;;
22
+    esac
23
+    echo "tf_testflt -n $name -i $t_in -O $o_out grep $args"
24
+}
25
+
26
+tf_do_subtests

+ 3
- 0
utils/tfkit/doc/templates/grep_engine/oracle/basic.stdout View File

@@ -0,0 +1,3 @@
1
+Linda and Nina work together.
2
+The mask *.* matches all.
3
+The mask *.docx matches Word documents.

+ 3
- 0
utils/tfkit/doc/templates/grep_engine/oracle/extended.stdout View File

@@ -0,0 +1,3 @@
1
+Alice uses Google.
2
+Bob wears goggles.
3
+Linda and Nina work together.

+ 1
- 0
utils/tfkit/doc/templates/grep_engine/oracle/fixed.stdout View File

@@ -0,0 +1 @@
1
+The mask *.* matches all.

+ 6
- 0
utils/tfkit/doc/templates/grep_engine/test/ALL.stdin View File

@@ -0,0 +1,6 @@
1
+Alice uses Google.
2
+Bob wears goggles.
3
+Joe and John are friends.
4
+Linda and Nina work together.
5
+The mask *.* matches all.
6
+The mask *.docx matches Word documents.

+ 135
- 0
utils/tfkit/include/common.sh View File

@@ -0,0 +1,135 @@
1
+#
2
+# Variables to hold exit status semantic
3
+#
4
+TF_ES_OK=0
5
+TF_ES_FAIL=1
6
+TF_ES_BAILOUT=2
7
+TF_ES_ERROR=3
8
+TF_ES_PANIC=4
9
+
10
+#
11
+# Option to turn on/off verbosity
12
+#
13
+TF_VERBOSE=${TF_VERBOSE:-true}
14
+
15
+#
16
+# Option to turn on/off debug mode
17
+#
18
+TF_DEBUG=${TF_DEBUG:-false}
19
+
20
+#
21
+# Regex to filter test names to run
22
+#
23
+TF_FILTER_TEST="${TF_FILTER_TEST:-.*}"
24
+
25
+#
26
+# Regex to filter subtest names to run
27
+#
28
+TF_FILTER_SUBTEST="${TF_FILTER_SUBTEST:-.*}"
29
+
30
+#
31
+# Enable color?
32
+#
33
+TF_COLOR=${TF_COLOR:-true}
34
+
35
+#
36
+# Color definition variables
37
+#
38
+TF_COLOR_BLACK="\033[0;30m"
39
+TF_COLOR_RED="\033[0;31m"
40
+TF_COLOR_GREEN="\033[0;32m"
41
+TF_COLOR_YELLOW="\033[0;33m"
42
+TF_COLOR_BLUE="\033[0;34m"
43
+TF_COLOR_MAGENTA="\033[0;35m"
44
+TF_COLOR_CYAN="\033[0;36m"
45
+TF_COLOR_WHITE="\033[0;37m"
46
+TF_COLOR_LBLACK="\033[1;30m"
47
+TF_COLOR_LRED="\033[1;31m"
48
+TF_COLOR_LGREEN="\033[1;32m"
49
+TF_COLOR_LYELLOW="\033[1;33m"
50
+TF_COLOR_LBLUE="\033[1;34m"
51
+TF_COLOR_LMAGENTA="\033[1;35m"
52
+TF_COLOR_LCYAN="\033[1;36m"
53
+TF_COLOR_LWHITE="\033[1;37m"
54
+TF_COLOR_NONE="\033[1;0m"
55
+
56
+
57
+tf_exit_ok() {
58
+    #
59
+    # Exit with OK status
60
+    #
61
+    exit $TF_ES_OK
62
+}
63
+
64
+tf_exit_fail() {
65
+    #
66
+    # Warn $1 and exit with status FAIL
67
+    #
68
+    tf_warn "$@"
69
+    exit $TF_ES_FAIL
70
+}
71
+
72
+tf_exit_bailout() {
73
+    #
74
+    # Warn $1 and exit with status FAIL
75
+    #
76
+    tf_warn "$@"
77
+    exit $TF_ES_BAILOUT
78
+}
79
+
80
+tf_exit_error() {
81
+    #
82
+    # Warn $1 and exit with status FAIL
83
+    #
84
+    tf_warn "$@"
85
+    exit $TF_ES_ERROR
86
+}
87
+
88
+tf_exit_panic() {
89
+    #
90
+    # Warn $1 and exit with status FAIL
91
+    #
92
+    tf_warn "$@"
93
+    exit $TF_ES_PANIC
94
+}
95
+
96
+tf_debug() {
97
+    #
98
+    # Emit debug message
99
+    #
100
+    $TF_DEBUG || return 0
101
+    local msg
102
+    for msg in "$@";
103
+    do
104
+        $TF_COLOR && echo -ne "$TF_COLOR_CYAN" >&2
105
+        echo "||| $msg" >&2;
106
+        $TF_COLOR && echo -ne "$TF_COLOR_NONE" >&2
107
+    done
108
+}
109
+
110
+tf_think() {
111
+    #
112
+    # Emit status/progress message
113
+    #
114
+    $TF_VERBOSE || return 0
115
+    local msg
116
+    for msg in "$@";
117
+    do
118
+        $TF_COLOR && echo -ne "$TF_COLOR_LBLACK" >&2
119
+        echo "$pfx$msg$sfx" >&2;
120
+        $TF_COLOR && echo -ne "$TF_COLOR_NONE" >&2
121
+    done
122
+}
123
+
124
+tf_warn() {
125
+    #
126
+    # Emit warning
127
+    #
128
+    local msg
129
+    for msg in "$@";
130
+    do
131
+        $TF_COLOR && echo -ne "$TF_COLOR_LRED" >&2
132
+        echo "$msg" >&2;
133
+        $TF_COLOR && echo -ne "$TF_COLOR_NONE" >&2
134
+    done
135
+}

+ 117
- 0
utils/tfkit/include/harness.sh View File

@@ -0,0 +1,117 @@
1
+#!/bin/bash
2
+# ffoo test harness
3
+# See LICENSE file for copyright and license details.
4
+
5
+. "$TF_DIR/include/common.sh"
6
+
7
+#
8
+# Default path to header generator
9
+#
10
+__TF_HDRCMD="$TF_SUITE/TF_HEADER"
11
+
12
+
13
+__tf_collect_if_needed() {
14
+    #
15
+    # Collect artifact if exit status suggests it
16
+    #
17
+    # Use test exit status $1 to help decide if artifacts are
18
+    # needed, and collect them if so.
19
+    #
20
+    # If TF_COLLECT is set to "always", collect regardless of
21
+    # the status.  If set to "never", do not collect anything.
22
+    # The default setting, "auto" collects unless status is 0
23
+    # or 1 (pass or bailout); in that case do nothing.
24
+    #
25
+    local tes=$1    # test exit status
26
+    local will      # should we collect artifacts?
27
+    case "$TF_COLLECT:$tes" in
28
+        always:*)   will=true ;;
29
+        never:*)    will=false ;;
30
+        auto:0)     will=false ;;
31
+        auto:2)     will=false ;;
32
+        auto:*)     will=true ;;
33
+        *)          tf_exit_bailout "bad value of TF_COLLECT: $TF_COLLECT" ;;
34
+    esac
35
+    $will || return 0
36
+    mkdir -p "$artifact_dir/$stamp"
37
+    cp -r "$tmpdir"/* "$artifact_dir/$stamp"
38
+}
39
+
40
+__tf_default_header() {
41
+    #
42
+    # Create default header
43
+    #
44
+    echo "(add $__TF_HDRCMD executable for own header)"
45
+}
46
+
47
+__tf_header() {
48
+    #
49
+    # create header to add to output before test
50
+    #
51
+    local field
52
+    local hdrcmd="$__TF_HDRCMD"
53
+    test -x "$hdrcmd" || hdrcmd="__tf_default_header"
54
+    $hdrcmd \
55
+      | while read field;
56
+        do
57
+            test -n "$field" || break
58
+            tf_think "# $field"
59
+        done
60
+    tf_think ""
61
+}
62
+
63
+tf_enum_tests() {
64
+    #
65
+    # List what looks like test; relative to $TF_SUITE
66
+    #
67
+    tf_debug "TF_SUITE='$TF_SUITE'"
68
+    test -d "$TF_SUITE" || return 0
69
+    find -L \
70
+        "$TF_SUITE" \
71
+        -mindepth 2 \
72
+        -maxdepth 2 \
73
+        -type f \
74
+        -perm /111 \
75
+        -name TF_RUN \
76
+      | cut -d/ -f2
77
+}
78
+
79
+tf_run_tests() {
80
+    #
81
+    # Discover and run tests
82
+    #
83
+    local es=0          # overall exit status
84
+    local tmpdir=""     # test temporary dir
85
+    local tname=""      # test name
86
+    local tes=0         # test result
87
+    local stamp=""      # test stamp to use as artifact name
88
+    local tf_dir tf_suite   # to keep absolute paths for TF_RUN
89
+    local artifact_dir="$(readlink -f "$TF_ARTIFACTS")"
90
+    __tf_header
91
+    tf_debug "TF_VERSION='$TF_VERSION'"
92
+    tf_dir="$(readlink -f "$TF_DIR")"
93
+    tf_suite="$(readlink -f "$TF_SUITE")"
94
+    es=0
95
+    for tname in $(tf_enum_tests | grep -e "$TF_FILTER_TEST");
96
+    do
97
+        tf_think "... $tname"
98
+        tmpdir=$(mktemp -d)
99
+        stamp=$(date "+artifacts-$tname-%Y%m%d-%H%M%S")
100
+        cp -r "$TF_SUITE/$tname/"* "$tmpdir"
101
+        pushd "$tmpdir" >/dev/null
102
+            TF_DIR="$tf_dir" TF_SUITE=$tf_suite TF_TNAME="$tname" \
103
+                ./TF_RUN
104
+            tes=$?
105
+            __tf_collect_if_needed $tes
106
+            test $tes -gt $es && es=$tes
107
+        popd >/dev/null
108
+        rm -rf "$tmpdir"
109
+        if test $tes -eq 0;
110
+        then
111
+            tf_think "''' $tname ($tes)"
112
+        else
113
+            tf_warn "??? $tname ($tes)"
114
+        fi
115
+    done
116
+    return $es
117
+}

+ 73
- 0
utils/tfkit/include/subtest.sh View File

@@ -0,0 +1,73 @@
1
+#!/bin/bash
2
+
3
+. "$TF_DIR/include/common.sh"
4
+
5
+tf_enum_subtests() {
6
+    #
7
+    # Stub: enumerate subtests
8
+    #
9
+    tf_warn "implement tf_enum_subtests()!"
10
+    return $TF_ES_ERROR
11
+}
12
+
13
+tf_name2cmd() {
14
+    #
15
+    # Stub: expand test name to test command
16
+    #
17
+    tf_warn "implement tf_name2cmd()!"
18
+    return $TF_ES_ERROR
19
+}
20
+
21
+tf_do_subtest() {
22
+    #
23
+    # Run single subtest inc. setup/cleanup if present
24
+    #
25
+    local subtname="$1"     # this subtest name
26
+    local ses=0             # subtest exit status
27
+    local tcmd=""           # test command
28
+    local setup=true        # setup command
29
+    local cleanup=true      # cleanup command
30
+    test -f TF_SETUP   && setup=". TF_SETUP"
31
+    test -f TF_CLEANUP && cleanup=". TF_CLEANUP"
32
+    if $setup;
33
+    then
34
+        tcmd="$(tf_name2cmd "$subtname")"
35
+        tf_debug "tcmd='$tcmd'"
36
+        $tcmd; ses=$?
37
+    else
38
+        tf_warn "setup phase failed, skipping: $subtname"
39
+        ses=$TF_ES_ERROR
40
+    fi
41
+    if ! $cleanup;
42
+    then
43
+        tf_warn "cleanup phase failed: $subtname"
44
+        ses=$TF_ES_PANIC
45
+    fi
46
+    return $ses
47
+}
48
+
49
+tf_do_subtests() {
50
+    #
51
+    # Run all subtests and return highest status
52
+    #
53
+    local es=0              # final exit status ("worst" of subtests)
54
+    local subtname=""       # one subtest name
55
+    local tes=""            # one subtest exit status
56
+    local enumd=TF_ENUMERATED_SUBTESTS
57
+    local fltrd=TF_FILTERED_SUBTESTS
58
+    tf_enum_subtests >$enumd    || { tf_warn "error enumerating subtests"; return $TF_ES_BAILOUT; }
59
+    test -s $enumd              || { tf_warn "no subtests enumerated";     return $TF_ES_BAILOUT; }
60
+    grep -e "$TF_FILTER_SUBTEST" $enumd > $fltrd
61
+    test -s $fltrd  || tf_debug "TF_FILTER_SUBTEST ate everything: $TF_FILTER_SUBTEST"
62
+    for subtname in $(<$fltrd);
63
+    do
64
+        export TF_SUBTNAME=$subtname
65
+        tf_think "::: $TF_TNAME::$TF_SUBTNAME"
66
+        tf_do_subtest "$TF_SUBTNAME";
67
+        tes=$?
68
+        test $tes -gt $es            && es=$tes
69
+        test $tes -gt $TF_ES_OK      && tf_warn "!!! $TF_TNAME::$TF_SUBTNAME ($tes)"
70
+        test $tes -gt $TF_ES_BAILOUT && break
71
+    done
72
+    return $es
73
+}

+ 83
- 0
utils/tfkit/include/tools.sh View File

@@ -0,0 +1,83 @@
1
+#!/bin/bash
2
+
3
+. "$TF_DIR/include/common.sh"
4
+
5
+# 1. exec: [test] -> [result]
6
+# 2. eval:           [result] == [oracle]
7
+
8
+tf_testflt() {
9
+    #
10
+    # Run a simple test for a unix filter
11
+    #
12
+    #     tf_testflt -n foo [-i foo.stdin] \
13
+    #                [-O ffoo.stdout] [-E ffoo.stderr] [-S 3] \
14
+    #                cmd arg...
15
+    #
16
+    # Will drop *result/NAME.stdout* and *result/NAME.stderr* (intentionally
17
+    # not cleaning up).
18
+    #
19
+
20
+    # defaults
21
+    #
22
+    local t_in="/dev/null"      # test: stdin
23
+    local t_name=""             # test: name
24
+                                # command is "$@" after arg parsing
25
+    local t_es="0"              # final test exit status
26
+    local o_out="/dev/null"     # oracle: stdout
27
+    local o_err="/dev/null"     # oracle: stderr
28
+    local o_es="0"              # oralce: exit status
29
+    local r_out r_err r_es      # result: ^ ^ ^ those 3
30
+
31
+    # get args
32
+    #
33
+    local orig_args="$0 $*"
34
+    tf_debug "orig_args=$orig_args"
35
+    local arg_err=false
36
+    while true; do case "$1" in
37
+        -i) t_in="$2";          shift 2 || { arg_err=true; break; } ;;
38
+        -n) t_name="$2";        shift 2 || { arg_err=true; break; } ;;
39
+        -O) o_out="$2";         shift 2 || { arg_err=true; break; } ;;
40
+        -E) o_err="$2";         shift 2 || { arg_err=true; break; } ;;
41
+        -S) o_es="$2";          shift 2 || { arg_err=true; break; } ;;
42
+        --)                     shift; break ;;
43
+        "")                            break ;;
44
+        -*) tf_warn "wrong testcli arg: $1"; return $TF_ES_BAILOUT ;;
45
+        *)                             break ;;
46
+    esac done
47
+    $arg_err && { tf_warn "error parsing arguments: $orig_args"; return $TF_ES_BAILOUT; }
48
+    tf_debug "t_in='$t_in'"
49
+    tf_debug "t_name='$t_name'"
50
+    tf_debug "o_out='$o_out'"
51
+    tf_debug "o_err='$o_err'"
52
+    tf_debug "o_es='$o_es'"
53
+    tf_debug "test command: $*"
54
+    test "$t_in" = "-" && t_in=/dev/stdin   # works better for check below
55
+    test -z "$t_name"  && { tf_warn "missing test name"             ; return $TF_ES_BAILOUT; }
56
+    test -z "$1"       && { tf_warn "missing test command"          ; return $TF_ES_BAILOUT; }
57
+    test -r "$t_in"    || { tf_warn "missing input file: $t_in"     ; return $TF_ES_BAILOUT; }
58
+    test -e "$o_out"   || { tf_warn "missing oracle stdout: $o_out" ; return $TF_ES_BAILOUT; }
59
+    test -e "$o_err"   || { tf_warn "missing oracle stderr: $o_err" ; return $TF_ES_BAILOUT; }
60
+    test "$o_es" -ge 0 || { tf_warn "invalid oracle status: $o_es"  ; return $TF_ES_BAILOUT; }
61
+
62
+    # prepare
63
+    #
64
+    mkdir -p result
65
+    r_out="result/$t_name.stdout"
66
+    r_err="result/$t_name.stderr"
67
+    tf_debug "r_out='$r_out'"
68
+    tf_debug "r_err='$r_err'"
69
+    touch "$r_out" || { tf_warn "cannot create tmp file: $r_out" ; return $TF_ES_BAILOUT; }
70
+    touch "$r_err" || { tf_warn "cannot create tmp file: $r_err" ; return $TF_ES_PANIC; }
71
+
72
+    # run
73
+    #
74
+    ( <"$t_in" eval "$@" >"$r_out" 2>"$r_err" ); r_es=$?
75
+    tf_debug "r_es='$r_es'"
76
+
77
+    # eval/report/exit
78
+    #
79
+    test $r_es = $o_es || { tf_warn "bad exit status: $r_es (need $o_es)" ; t_es=$TF_ES_FAIL; }
80
+    diff -u "$o_err" "$r_err" || t_es=$TF_ES_FAIL
81
+    diff -u "$o_out" "$r_out" || t_es=$TF_ES_FAIL
82
+    return $t_es
83
+}

+ 45
- 0
utils/tfkit/runtests View File

@@ -0,0 +1,45 @@
1
+#!/bin/bash
2
+# ffoo test framework
3
+# See LICENSE file for copyright and license details.
4
+
5
+export TF_VERSION="0.0.4"
6
+
7
+die() {
8
+    echo "$@" && exit 9
9
+}
10
+
11
+export LC_ALL=C
12
+export TF_DIR=${TF_DIR:-$(dirname "$0")}
13
+export TF_SUITE="${TF_SUITE:-tests}"
14
+export TF_ARTIFACTS="${TF_ARTIFACTS:-tfkit-artifacts}"
15
+export TF_COLLECT="${TF_COLLECT:-auto}"
16
+
17
+
18
+usage() {
19
+    echo "usage: $(basename "$0") [-c|-C] [-t tests_re] [-s subtest_re] [-p binpath] [-v] [-d]" >&2
20
+    exit 2
21
+}
22
+
23
+version() {
24
+    echo "TFKit (A trivial test kit) $TF_VERSION"
25
+    exit 0
26
+}
27
+
28
+while true; do case "$1" in
29
+    -c|--collect)           TF_COLLECT=always; shift ;;
30
+    -C|--no-collect)        TF_COLLECT=never; shift ;;
31
+    -t|--filter-test)       export TF_FILTER_TEST="$2"; shift 2 ;;
32
+    -s|--filter-subtest)    export TF_FILTER_SUBTEST="$2"; shift 2 ;;
33
+    -p|--prefix)            export PATH="$(readlink -f "$2")/bin:$PATH"; shift 2 ;;
34
+    -d|--debug)             export TF_DEBUG=true; shift ;;
35
+    -v|--verbose)           export TF_VERBOSE=true; shift ;;
36
+    --version)              version ;;
37
+    --version-semver)       echo "$TF_VERSION"; exit 0 ;;
38
+    "") break ;;
39
+    *)  usage ;;
40
+esac done
41
+
42
+. "$TF_DIR/include/harness.sh" \
43
+ || die "cannot import harness; is TF_DIR set properly?: $TF_DIR"
44
+
45
+time tf_run_tests

+ 18
- 0
utils/tfkit/tfkit.mk View File

@@ -0,0 +1,18 @@
1
+# tfkit - Fastfoo - Bash dot on steroids
2
+# See LICENSE file for copyright and license details.
3
+#
4
+# To use, set TF_DIR (make macro) in your Makefile and
5
+# include this file.
6
+#
7
+# Adds `test` target.
8
+#
9
+# Note that running "runtests" directly should have the same
10
+# effect but you will need to set TF_DIR and/or TF_SUITE properly
11
+# unless they are default (see README)
12
+
13
+export TF_DIR
14
+
15
+test:
16
+	@$(TF_DIR)/runtests
17
+
18
+.PHONY: test