summaryrefslogtreecommitdiffstats
path: root/tests/util.sh
blob: 76b72b90e4b85b0dc1f2f4afd1f6bdc8a12800ee (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
#!/hint/bash

# Copyright © Tavian Barnes <tavianator@tavianator.com>
# SPDX-License-Identifier: 0BSD

## Utility functions

# Portable realpath(1)
_realpath() (
    cd "$(dirname -- "$1")"
    echo "$PWD/$(basename -- "$1")"
)

# Globals
ROOT=$(_realpath "$(dirname -- "$TESTS")")
TESTS="$ROOT/tests"
BIN="$ROOT/bin"
MKSOCK="$BIN/tests/mksock"
XTOUCH="$BIN/tests/xtouch"
UNAME=$(uname)

# Standardize the environment
stdenv() {
    export LC_ALL=C
    export TZ=UTC0

    local SAN_OPTIONS="abort_on_error=1:halt_on_error=1:log_to_syslog=0"
    export ASAN_OPTIONS="$SAN_OPTIONS"
    export LSAN_OPTIONS="$SAN_OPTIONS"
    export MSAN_OPTIONS="$SAN_OPTIONS"
    export TSAN_OPTIONS="$SAN_OPTIONS"
    export UBSAN_OPTIONS="$SAN_OPTIONS"

    export LS_COLORS=""
    unset BFS_COLORS

    if [ "$UNAME" = Darwin ]; then
        # ASan on macOS likes to report
        #
        #     malloc: nano zone abandoned due to inability to preallocate reserved vm space.
        #
        # to syslog, which as a side effect opens a socket which might take the
        # place of one of the standard streams if the process is launched with
        # it closed.  This environment variable avoids the message.
        export MallocNanoZone=0
    fi

    # Count the inherited FDs
    if [ -d /proc/self/fd ]; then
        local fds=/proc/self/fd
    else
        local fds=/dev/fd
    fi
    # We use ls $fds on purpose, rather than e.g. ($fds/*), to avoid counting
    # internal bash fds that are not exposed to spawned processes
    NOPENFD=$(ls -1q "$fds/" 2>/dev/null | wc -l)
    NOPENFD=$((NOPENFD > 3 ? NOPENFD - 1 : 3))

    # Close stdin so bfs doesn't think we're interactive
    # dup() the standard fds for logging even when redirected
    exec </dev/null {DUPOUT}>&1 {DUPERR}>&2

    # Get the ttyname
    if [ -t $DUPOUT ]; then
        TTY=$(tty <&$DUPOUT)
    elif [ -t $DUPERR ]; then
        TTY=$(tty <&$DUPERR)
    else
        TTY=
    fi
}

# Drop root priviliges or bail
drop_root() {
    if command -v capsh &>/dev/null; then
        if capsh --has-p=cap_dac_override &>/dev/null || capsh --has-p=cap_dac_read_search &>/dev/null; then
	    if [ -n "${BFS_TRIED_DROP:-}" ]; then
                color cat >&2 <<EOF
${RED}error:${RST} Failed to drop capabilities.
EOF

	        exit 1
	    fi

            color cat >&2 <<EOF
${YLW}warning:${RST} Running as ${BLD}$(id -un)${RST} is not recommended.  Dropping ${BLD}cap_dac_override${RST} and
${BLD}cap_dac_read_search${RST}.

EOF

            BFS_TRIED_DROP=y exec capsh \
                --drop=cap_dac_override,cap_dac_read_search \
                --caps=cap_dac_override,cap_dac_read_search-eip \
                -- "$0" "$@"
        fi
    elif ((EUID == 0)); then
        UNLESS=
        if [ "$UNAME" = "Linux" ]; then
	    UNLESS=" unless ${GRN}capsh${RST} is installed"
        fi

        color cat >&2 <<EOF
${RED}error:${RST} These tests expect filesystem permissions to be enforced, and therefore
will not work when run as ${BLD}$(id -un)${RST}${UNLESS}.
EOF
        exit 1
    fi
}

## Debugging

# Get the bash call stack
callers() {
    local frame=0
    while caller $frame; do
        ((++frame))
    done
}

# Print a message including path, line number, and command
debug() {
    local file="$1"
    local line="$2"
    local msg="$3"
    local cmd="$(awk "NR == $line" "$file" 2>/dev/null)" || :
    file="${file/#*\/tests\//tests/}"

    color printf "${BLD}%s:%d:${RST} %s\n    %s\n" "$file" "$line" "$msg" "$cmd"
}

## Deferred cleanup

# Quote a command safely for eval
quote() {
    printf '%q' "$1"
    shift
    if (($# > 0)); then
        printf ' %q' "$@"
    fi
}

DEFER_LEVEL=-1

# Run a command when this (sub)shell exits
defer() {
    # Check if the EXIT trap is already set
    if ((DEFER_LEVEL != BASH_SUBSHELL)); then
        DEFER_LEVEL=$BASH_SUBSHELL
        DEFER_CMDS=()
        DEFER_LINES=()
        DEFER_FILES=()
        trap pop_defers EXIT
    fi

    DEFER_CMDS+=("$(quote "$@")")

    local line file
    read -r line file < <(caller)
    DEFER_LINES+=("$line")
    DEFER_FILES+=("$file")
}

# Pop a single command from the defer stack and run it
pop_defer() {
    local cmd="${DEFER_CMDS[-1]}"
    local file="${DEFER_FILES[-1]}"
    local line="${DEFER_LINES[-1]}"
    unset "DEFER_CMDS[-1]"
    unset "DEFER_FILES[-1]"
    unset "DEFER_LINES[-1]"

    local ret=0
    eval "$cmd" || ret=$?

    if ((ret != 0)); then
        debug "$file" $line "${RED}error $ret${RST}" >&$DUPERR
    fi

    return $ret
}

# Run all deferred commands
pop_defers() {
    local ret=0

    while ((${#DEFER_CMDS[@]} > 0)); do
        pop_defer || ret=$?
    done

    return $ret
}