Skip to content

Commit 13717b0

Browse files
committed
Rewrite expect script in C to avoid multiple issues with shell
1 parent 417e27d commit 13717b0

File tree

5 files changed

+217
-54
lines changed

5 files changed

+217
-54
lines changed

app/ext_example/Makefile

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ else
6161
CFLAGS += -g
6262
endif
6363

64-
export THIS_LIB_BASE=$(shell cd ../.. && pwd)
64+
THIS_LIB_BASE=$(shell cd ../.. && pwd)
6565
CCBN=$(shell basename ${CC})
6666
BUILD_DIR=${THIS_LIB_BASE}/build/${BUILD_SUBDIR}/${CCBN}
6767
TARGET=${BUILD_DIR}/bin/zsvextmy.${SO}
@@ -77,9 +77,15 @@ TEST_PASS=printf "${COLOR_BLUE}$@: ${COLOR_GREEN}Passed${COLOR_NONE}\n"
7777
TEST_FAIL=(printf "${COLOR_BLUE}$@: ${COLOR_RED}Failed!${COLOR_NONE}\n" && exit 1)
7878
TEST_INIT=printf "${COLOR_PINK}$@: ${COLOR_NONE}\n"
7979

80+
# test-expect.sh needs some of these to be exported
81+
export EXPECT_TIMEOUT ?= 5
8082
EXPECT=../../scripts/test-expect.sh
83+
export EXPECT_BIN=${BUILD_DIR}/bin/test-expect${EXE}
8184
export EXPECTED_PATH=test/expected
8285

86+
${EXPECT_BIN}: ${THIS_LIB_BASE}/util/expect.c
87+
${CC} ${CFLAGS} -Wall -o $@ $<
88+
8389
CFLAGS_SHARED=-shared
8490
ifneq ($(findstring emcc,$(CC)),) # emcc
8591
CFLAGS_SHARED=-s SIDE_MODULE=1 -s LINKABLE=1
@@ -145,11 +151,17 @@ test-3: test-%: ${CLI} ${TARGET}
145151
make -C ../test worldcitiespop_mil.csv
146152

147153
export TMP_DIR=/tmp
148-
DATE_TIME:=$(shell date +%F-%H-%M-%S)
149-
export TIMINGS_CSV:=${TMP_DIR}/timings-${DATE_TIME}.csv
154+
export TIMINGS_CSV:=${TMP_DIR}/timings.csv
155+
156+
${TIMINGS_CSV}:
157+
@mkdir -p ${TMP_DIR}
158+
@echo -n "Git, Test, Stage, Date, Time, Elapsed (s)" > ${TIMINGS_CSV}
159+
160+
test-sheet-extension-1 test-sheet-extension-2: ${TARGET} ${TIMINGS_CSV} ${EXPECT_BIN}
161+
150162
export CMP=cmp
151163
TMUX_TERM=xterm-256color
152-
test-sheet-extension-1: ${CLI} ${TARGET} ../test/worldcitiespop_mil.csv
164+
test-sheet-extension-1: ${CLI} ../test/worldcitiespop_mil.csv
153165
@${TEST_INIT}
154166
@rm -f ${TMP_DIR}/zsvext-$@.out tmux-*.log
155167
@tmux kill-session -t $@ || echo 'No tmux session to kill'
@@ -161,7 +173,7 @@ test-sheet-extension-1: ${CLI} ${TARGET} ../test/worldcitiespop_mil.csv
161173
tmux send-keys -t $@ "t" "hello" Enter && \
162174
${EXPECT} $@ && ${TEST_PASS} || ${TEST_FAIL})
163175

164-
test-sheet-extension-2: ${CLI} ${TARGET}
176+
test-sheet-extension-2: ${CLI}
165177
@${TEST_INIT}
166178
@rm -f ${TMP_DIR}/zsvext-$@.out tmux-*.log
167179
@tmux kill-session -t $@ || echo 'No tmux session to kill'

app/test/Makefile

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,10 @@ else
3939
EXE=.exe
4040
endif
4141

42-
export THIS_LIB_BASE=$(shell cd ../.. && pwd)
42+
THIS_LIB_BASE=$(shell cd ../.. && pwd)
4343
CCBN=$(shell basename ${CC})
4444
BUILD_DIR=${THIS_LIB_BASE}/build/${BUILD_SUBDIR}/${CCBN}
45+
# exported for expect
4546
export TMP_DIR=${THIS_LIB_BASE}/tmp
4647
TEST_DATA_DIR=${THIS_LIB_BASE}/data
4748

@@ -87,14 +88,18 @@ else
8788
endif
8889

8990
BIG_FILE ?= none
90-
EXPECT_TIMEOUT ?= 5
91+
# test-expect.sh needs some of these to be exported
92+
export EXPECT_TIMEOUT ?= 5
9193
EXPECT=../../scripts/test-expect.sh
94+
export EXPECT_BIN=${BUILD_DIR}/bin/test-expect${EXE}
9295
export EXPECTED_PATH=expected
9396

97+
${EXPECT_BIN}: ${THIS_LIB_BASE}/util/expect.c
98+
${CC} ${CFLAGS} -Wall -o $@ $<
99+
94100
MAKE_BIN=$(notdir ${MAKE})
95101

96-
DATE_TIME:=$(shell date +%F-%H-%M-%S)
97-
export TIMINGS_CSV:=${TMP_DIR}/timings-${DATE_TIME}.csv
102+
export TIMINGS_CSV:=${TMP_DIR}/timings.csv
98103

99104
help:
100105
@echo "To run all tests: ${MAKE_BIN} test [LEAKS=1]"
@@ -116,7 +121,7 @@ test-paste:
116121

117122
${TIMINGS_CSV}:
118123
@mkdir -p ${TMP_DIR}
119-
@echo -n "Test, Stage, Time" > ${TIMINGS_CSV}
124+
@echo -n "Git, Test, Stage, Date, Time, Elapsed (s)" > ${TIMINGS_CSV}
120125

121126
.SECONDARY: worldcitiespop_mil.csv
122127

@@ -613,7 +618,9 @@ test-sheet-cleanup:
613618
@rm -f tmux-*.log
614619
@tmux kill-server || printf ''
615620

616-
test-sheet-all: test-sheet-1 test-sheet-2 test-sheet-3 test-sheet-4 test-sheet-5 test-sheet-6 test-sheet-7 test-sheet-8 test-sheet-9 test-sheet-10 test-sheet-11 test-sheet-12 test-sheet-13 test-sheet-14 test-sheet-subcommand test-sheet-prop-cmd-opt
621+
ALL_SHEET_TESTS=test-sheet-1 test-sheet-2 test-sheet-3 test-sheet-4 test-sheet-5 test-sheet-6 test-sheet-7 test-sheet-8 test-sheet-9 test-sheet-10 test-sheet-11 test-sheet-12 test-sheet-13 test-sheet-14 test-sheet-subcommand test-sheet-prop-cmd-opt
622+
${ALL_SHEET_TESTS}: ${BUILD_DIR}/bin/zsv_sheet${EXE} ${TIMINGS_CSV} ${EXPECT_BIN}
623+
test-sheet-all: ${ALL_SHEET_TESTS}
617624
@(for SESSION in $^; do ! tmux kill-session -t "$$SESSION" 2>/dev/null; done && ${TEST_PASS} || ${TEST_FAIL})
618625

619626
TMUX_TERM=xterm-256color

scripts/test-expect.sh

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,46 @@
11
#!/bin/sh -eu
22

3-
script_dir=$(dirname "$0")
3+
# The Makefile target which will be the test name
4+
TARGET="$1"
45

5-
export TARGET="$1"
6+
# The intermediate test stage if the test is split into multiple stages
7+
# if it is blank then it is the last stage
68
if [ -z "${2:-}" ]; then
7-
export STAGE=""
9+
STAGE=""
810
else
9-
export STAGE=-"$2"
11+
STAGE=-"$2"
1012
fi
11-
export CAPTURE="${TMP_DIR}/$TARGET$STAGE".out
12-
EXPECTED="$EXPECTED_PATH/$TARGET$STAGE".out
13-
export EXPECTED
14-
matched=false
15-
16-
t=${EXPECT_TIMEOUT:-5}
17-
18-
cleanup() {
19-
if $matched; then
20-
if [ -z "$STAGE" ]; then
21-
tmux send-keys -t "$TARGET" "q"
22-
fi
23-
exit 0
24-
fi
25-
26-
tmux send-keys -t "$TARGET" "q"
27-
echo 'Incorrect output:'
28-
cat "$CAPTURE"
29-
${CMP} -s "$CAPTURE" "$EXPECTED"
30-
exit 1
31-
}
3213

33-
trap cleanup INT TERM QUIT
14+
# The capture file that is written to if we fail to match. If the capture is correct
15+
# but expected is missing or wrong, then we can copy this to the expected location
16+
CAPTURE="${TMP_DIR}/$TARGET$STAGE".out
17+
# The expected output to match against
18+
EXPECTED="$EXPECTED_PATH/$TARGET$STAGE".out
3419

35-
printf "\n%s, %s" "$TARGET" "${2:-}" >> "${TIMINGS_CSV}"
20+
# write Git hash, target, stage, date, time to the CSV timings file
21+
printf "\n%s, %s, %s, %s" "$(git rev-parse --short HEAD)" "$TARGET" "${2:-}" "$(date '+%F, %T')" >> "${TIMINGS_CSV}"
3622

23+
# temporarily disable error checking
3724
set +e
38-
match_time=$(time -p timeout -k $(( t + 1 )) $t "${script_dir}"/test-retry-capture-cmp.sh 2>&1)
25+
# run the expect utility which will repeatedly
26+
# try to match the expected output to the screen captured by tmux. If successfull
27+
# it will output the elapsed time to stdout and therefor match_time, on failure it
28+
# will print to stderr which the user will see
29+
match_time=$($EXPECT_BIN "$EXPECTED" "$TARGET" "$CAPTURE" "$EXPECT_TIMEOUT")
3930
status=$?
4031
set -e
4132

4233
if [ $status -eq 0 ]; then
43-
matched=true
44-
match_time=$(echo "$match_time" | head -n 1 | cut -f 2 -d ' ')
34+
# write the time it took to match the expected text with the capture
4535
echo "$TARGET$STAGE took $match_time"
4636
printf ", %s" "$match_time" >> "${TIMINGS_CSV}"
37+
else
38+
echo "$TARGET$STAGE did not match"
39+
fi
40+
41+
# Quit if this is the last stage or there was an error
42+
if [ -z "$STAGE" ] || [ $status -eq 1 ]; then
43+
tmux send-keys -t "$TARGET" "q"
4744
fi
4845

49-
cleanup
46+
exit $status

scripts/test-retry-capture-cmp.sh

Lines changed: 0 additions & 12 deletions
This file was deleted.

util/expect.c

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
#include <stdio.h>
2+
#include <stdlib.h>
3+
#include <string.h>
4+
#include <unistd.h>
5+
#include <time.h>
6+
7+
/*
8+
* This utility allows us to wait for a particular output from zsv sheet.
9+
*
10+
* It avoids needing to call sleep with a set time and instead we specify the output
11+
* we wish to see and a timeout. If the timeout is exceeded then it fails and prints
12+
* the output that was last seen and saves it to a file.
13+
*
14+
* It repeatedly calls tmux capture-pane and compares the contents with the expected file.
15+
*
16+
* There is a wrapper for this utility which is used in the Makefiles scripts/test-expect.sh
17+
*/
18+
19+
int main(int argc, char **argv) {
20+
if (argc != 5) {
21+
fprintf(stderr, "Usage: %s <expected> <pane> <actual> <timeout>\n", argv[0]);
22+
return 1;
23+
}
24+
25+
char *expected = argv[1];
26+
char *pane = argv[2];
27+
char *actual = argv[3];
28+
char *timeout_s = argv[4];
29+
30+
char *endptr;
31+
double timeout = strtod(timeout_s, &endptr);
32+
33+
if (endptr == timeout_s) {
34+
perror("strtod");
35+
return 1;
36+
}
37+
38+
// Read the contents of the file
39+
FILE *file = fopen(expected, "r");
40+
if (file == NULL) {
41+
perror("fopen");
42+
return 1;
43+
}
44+
45+
if (fseek(file, 0, SEEK_END) != 0) {
46+
perror("fseek");
47+
return 1;
48+
}
49+
50+
long file_size = ftell(file);
51+
if (file_size < 0) {
52+
perror("ftell");
53+
return 1;
54+
}
55+
56+
rewind(file);
57+
58+
char *expected_output = malloc(file_size + 1);
59+
if (expected_output == NULL) {
60+
perror("malloc");
61+
return 1;
62+
}
63+
64+
size_t bytes_read = fread(expected_output, 1, file_size, file);
65+
if (bytes_read != (size_t)file_size) {
66+
if (ferror(file)) {
67+
perror("fread");
68+
return 1;
69+
}
70+
}
71+
72+
expected_output[file_size] = '\0';
73+
74+
if (fclose(file) != 0) {
75+
perror("fclose");
76+
return 1;
77+
}
78+
79+
struct timespec start_time;
80+
if (clock_gettime(CLOCK_MONOTONIC, &start_time) != 0) {
81+
perror("clock_gettime");
82+
return 1;
83+
}
84+
85+
struct timespec current_time;
86+
char *expected_input = malloc(file_size + 1);
87+
if (expected_input == NULL) {
88+
perror("malloc");
89+
return 1;
90+
}
91+
92+
double elapsed_time;
93+
char command[256];
94+
snprintf(command, sizeof(command), "tmux capture-pane -t %s -p", pane);
95+
96+
while (1) {
97+
// Run tmux capture-pane -p
98+
FILE *pipe = popen(command, "r");
99+
if (pipe == NULL) {
100+
perror("popen");
101+
return 1;
102+
}
103+
104+
size_t len = 0;
105+
for (int i = 0; i < 10 && len < (size_t)file_size; i++) {
106+
len += fread(expected_input + len, 1, file_size - len, pipe);
107+
if (ferror(pipe)) {
108+
perror("fread");
109+
return 1;
110+
}
111+
usleep(1);
112+
}
113+
114+
if (pclose(pipe) != 0) {
115+
return 1;
116+
}
117+
118+
// Check if the timeout has expired
119+
if (clock_gettime(CLOCK_MONOTONIC, &current_time) != 0) {
120+
perror("clock_gettime");
121+
return 1;
122+
}
123+
124+
elapsed_time =
125+
(current_time.tv_sec - start_time.tv_sec) + (current_time.tv_nsec - start_time.tv_nsec) / 1000000000.0;
126+
127+
// Check if the output matches the expected output
128+
if (strcmp(expected_input, expected_output) == 0) {
129+
break;
130+
}
131+
132+
if (elapsed_time > timeout) {
133+
fprintf(stderr, "Timeout expired after %.2f seconds\n", elapsed_time);
134+
fprintf(stderr, "Last output that failed to match:\n%s\n", expected_input);
135+
136+
FILE *output = fopen(actual, "w");
137+
if (output == NULL) {
138+
perror("fopen");
139+
return 1;
140+
}
141+
142+
fprintf(output, "%s", expected_input);
143+
fclose(output);
144+
145+
return 1;
146+
}
147+
148+
// Sleep for a short period of time before trying again
149+
struct timespec sleep_time = {0, 10000000}; // 10 milliseconds
150+
if (nanosleep(&sleep_time, NULL) != 0) {
151+
perror("nanosleep");
152+
return 1;
153+
}
154+
}
155+
156+
printf("%.2f\n", elapsed_time);
157+
158+
return 0;
159+
}

0 commit comments

Comments
 (0)