Commits

Alex Efros committed 0abd7bf Merge

merge upstream

  • Participants
  • Parent commits 1e31bde, 8aa4445

Comments (0)

Files changed (11)

 vcprompt: $(objects)
 	$(CC) -o $@ $(objects)
 
+# build a standalone version of capture_child() library for testing
+src/capture: src/capture.c src/capture.h src/common.c src/common.h
+	$(CC) -DTEST_CAPTURE $(CFLAGS) -o $@ src/capture.c src/common.c
+
 # Maximally pessimistic view of header dependencies.
 $(objects): $(headers)
 
-.PHONY: check check-simple check-hg check-git
-check: check-simple check-hg check-git
+.PHONY: check check-simple check-hg check-git check-fossil
+check: check-simple check-hg check-git check-fossil
 
 hgrepo = tests/hg-repo.tar
 gitrepo = tests/git-repo.tar
+fossilrepo = tests/fossil-repo
 
 check-simple: vcprompt
 	cd tests && ./test-simple
 $(gitrepo): tests/setup-git
 	cd tests && ./setup-git
 
+check-fossil: vcprompt $(fossilrepo)
+	cd tests && ./test-fossil
+
+$(fossilrepo): tests/setup-fossil
+	cd tests && ./setup-fossil
+
 clean:
 	rm -f $(objects) vcprompt $(hgrepo) $(gitrepo)
 
+#include "capture.h"
+#include "common.h"
+
+#include <errno.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/wait.h>
+#include <sys/select.h>
+#include <sys/types.h>
+
+static void
+init_dynbuf(dynbuf *dbuf, int bufsize)
+{
+    dbuf->size = bufsize;
+    dbuf->len = 0;
+    dbuf->buf = malloc(bufsize); /* caller handles NULL */
+    dbuf->eof = 0;
+}
+
+static ssize_t
+read_dynbuf(int fd, dynbuf *dbuf)
+{
+    size_t avail = dbuf->size - dbuf->len;
+    if (avail < 1024) {
+        dbuf->size *= 2;
+        dbuf->buf = realloc(dbuf->buf, dbuf->size);
+        avail = dbuf->size - dbuf->len;
+    }
+    /* read avail-1 bytes to leave room for termininating \0 */
+    ssize_t nread = read(fd, dbuf->buf + dbuf->len, avail - 1);
+    if (nread < 0)
+        return nread;
+    else if (nread == 0) {
+        dbuf->buf[dbuf->len] = '\0';
+        dbuf->eof = 1;
+        //debug("capture: eof on fd %d; total read = %d bytes", fd, dbuf->len);
+        return 0;
+    }
+    //debug("capture: read %d bytes from child via fd %d", nread, fd);
+    dbuf->len += nread;
+    return nread;
+}
+
+capture_t *
+new_capture()
+{
+    int bufsize = 4096;
+    capture_t *result = malloc(sizeof(capture_t));
+    if (result == NULL)
+        goto err;
+    init_dynbuf(&result->stdout, bufsize);
+    if (result->stdout.buf == NULL)
+        goto err;
+
+    init_dynbuf(&result->stderr, bufsize);
+    if (result->stderr.buf == NULL)
+        goto err;
+
+    return result;
+
+ err:
+    free_capture(result);
+    return NULL;
+}
+
+void
+free_capture(capture_t *result)
+{
+    if (result != NULL) {
+        if (result->stdout.buf != NULL)
+            free(result->stdout.buf);
+        if (result->stderr.buf != NULL)
+            free(result->stderr.buf);
+        free(result);
+    }
+}
+
+static void
+print_cmd(char *const argv[])
+{
+    int bufsize = 100;
+    char cmd[bufsize];
+    int offs = 0;
+    int i;
+
+    for (i = 0; argv[i] != NULL; i++) {
+        if (i > 0 && offs + 1 < bufsize) {
+            cmd[offs++] = ' ';
+            cmd[offs] = '\0';
+        }
+        int arglen = strlen(argv[i]);
+        /* + 4 to leave room for " ..." */
+        if (offs + arglen + 4 < bufsize) {
+            strcpy(cmd+offs, argv[i]);
+            offs += arglen;
+        }
+        else {
+            strcpy(cmd+offs, "...");
+            break;
+        }
+    }
+    debug("spawning child process: %s", cmd);
+}
+
+capture_t *
+capture_child(const char *file, char *const argv[])
+{
+    int stdout_pipe[] = {-1, -1};
+    int stderr_pipe[] = {-1, -1};
+    capture_t *result = NULL;
+    if (pipe(stdout_pipe) < 0)
+        goto err;
+    if (pipe(stderr_pipe) < 0)
+        goto err;
+
+    if (debug_mode())
+        print_cmd(argv);
+    pid_t pid = fork();
+    if (pid < 0) {
+        goto err;
+    }
+    if (pid == 0) {             /* in the child */
+        close(stdout_pipe[0]);  /* don't need the read ends of the pipes */
+        close(stderr_pipe[0]);
+        if (dup2(stdout_pipe[1], STDOUT_FILENO) < 0)
+            _exit(1);
+        if (dup2(stderr_pipe[1], STDERR_FILENO) < 0)
+            _exit(1);
+
+        execvp(file, argv);
+        debug("error executing %s: %s\n", file, strerror(errno));
+        _exit(127);
+    }
+
+    /* parent: don't need write ends of the pipes */
+    close(stdout_pipe[1]);
+    close(stderr_pipe[1]);
+
+    result = new_capture();
+    if (result == NULL)
+        goto err;
+
+    int cstdout = stdout_pipe[0];
+    int cstderr = stderr_pipe[0];
+
+    int done = 0;
+    while (!done) {
+        int maxfd = -1;
+        fd_set child_fds;
+        FD_ZERO(&child_fds);
+        if (!result->stdout.eof) {
+            FD_SET(cstdout, &child_fds);
+            maxfd = cstdout;
+        }
+        if (!result->stderr.eof) {
+            FD_SET(cstderr, &child_fds);
+            maxfd = cstderr;
+        }
+        int numavail = select(maxfd+1, &child_fds, NULL, NULL, NULL);
+        if (numavail < 0)
+            goto err;
+        else if (numavail == 0) /* EOF on both pipes */
+            break;
+
+        if (FD_ISSET(cstdout, &child_fds)) {
+            if (read_dynbuf(cstdout, &result->stdout) < 0)
+                goto err;
+        }
+        if (FD_ISSET(cstderr, &child_fds)) {
+            if (read_dynbuf(cstderr, &result->stderr) < 0)
+                goto err;
+        }
+        done = result->stdout.eof && result->stderr.eof;
+    }
+
+    int status;
+    waitpid(pid, &status, 0);
+    result->status = result->signal = 0;
+    if (WIFEXITED(status))
+        result->status = WEXITSTATUS(status);
+    else if (WIFSIGNALED(status))
+        result->signal = WTERMSIG(status);
+
+    if (result->status != 0)
+        debug("child process %s exited with status %d",
+              file, result->status);
+    if (result->signal != 0)
+        debug("child process %s killed by signal %d",
+              file, result->signal);
+    if (result->stderr.len > 0)
+        debug("child process %s wrote to stderr:\n%s",
+              file, result->stderr.buf);
+
+    return result;
+ err:
+    if (stdout_pipe[0] > -1)
+        close(stdout_pipe[0]);
+    if (stdout_pipe[1] > -1)
+        close(stdout_pipe[1]);
+    if (stderr_pipe[0] > -1)
+        close(stderr_pipe[0]);
+    if (stderr_pipe[1] > -1)
+        close(stderr_pipe[1]);
+    free_capture(result);
+    return NULL;
+}
+
+#if 0
+int
+capture_failed(capture_t *capture)
+{
+    return (capture == NULL || capture->status > 0 || capture->signal > 0);
+}
+#endif
+
+/*
+ * To build a standalone executable for testing:
+ *    make src/capture
+ *
+ * Then to actually test, commands like this are useful:
+ *    ./src/capture ls -l              # stdout only
+ *    ./src/capture ls -l asdf fsda    # stderr only
+ *    ./src/capture ls -l . fdsa / asdf # mix of stdout and stderr
+ *    ./src/capture sh -c "echo -n foobar ; echo -n bipbop >&2; echo whee ; echo fnorb >&2"
+ *    ./src/capture find /etc -type f
+ * ...and so forth.
+ */
+#ifdef TEST_CAPTURE
+#include <stdio.h>
+
+int
+main(int argc, char *argv[])
+{
+    if (argc < 2) {
+        fprintf(stderr, "usage: %s prog arg...\n", argv[0]);
+        return 2;
+    }
+    options_t options = {debug: 1};
+    set_options(&options);
+
+    capture_t *result = capture_child(argv[1], argv+1);
+    int status;
+    if (result == NULL) {
+        perror("capture failed");
+        return 1;
+    }
+    printf("read %ld bytes from child stdout: >%s<\n",
+           result->stdout.len, result->stdout.buf);
+    printf("read %ld bytes from child stderr: >%s<\n",
+           result->stderr.len, result->stderr.buf);
+    status = result->status;
+    printf("child status = %d, signal =%d\n", status, result->signal);
+    free_capture(result);
+    return status;
+}
+#endif
+#ifndef CAPTURE_H
+#define CAPTURE_H
+
+#include <sys/types.h>
+
+typedef struct {
+    size_t size;                /* bytes allocated */
+    size_t len;                 /* bytes filled */
+    char *buf;
+    int eof;
+} dynbuf;
+
+typedef struct {
+    dynbuf stdout;
+    dynbuf stderr;
+    int status;                 /* exit status that child passed (if any) */
+    int signal;                 /* signal that killed the child (if any) */
+} capture_t;
+
+/* fork() and exec() a child process, capturing its entire stdout and
+ * stderr to a capture object. Just like with execvp(), argv[0] should
+ * be file (unless you are playing funny games) and the last element
+ * of argv must be NULL.
+ *
+ * On return, capture->stdout.buf will be the child's stdout, and
+ * capture->stdout.len the number of bytes read. capture->stdout.buf
+ * is null terminated, so as long as the child's output is textual,
+ * you can use it as a string. Similarly, child's stderr is in
+ * capture->stderr.buf and capture->stderr.len.
+ */
+capture_t *
+capture_child(const char *file, char *const argv[]);
+
+/* free all resources in the object returned by capture_child() */
+void
+free_capture(capture_t *capture);
+
+#if 0
+/* return true if capture_child() failed: capture is NULL, or the
+ * child exited with non-zero status, or the child was killed by a signal
+ */
+int
+capture_failed(capture_t *capture);
+#endif
+
+#endif
     _options = options;
 }
 
+int debug_mode()
+{
+    return _options->debug;
+}
+
 int result_set_revision(result_t* result, const char *revision, int len)
 {
     if (result->revision)
 
 void
 set_options(options_t*);
+
+int
+debug_mode();
+
 vccontext_t*
 init_context(const char *name,
              options_t* options,
 #include <stdio.h>
 #include <string.h>
 #include <sys/wait.h>
+
 #include "fossil.h"
+#include "common.h"
+#include "capture.h"
 
 static int
 fossil_probe(vccontext_t* context)
 {
-    return isfile("_FOSSIL_");
+    return isfile("_FOSSIL_") || isfile(".fslckout");
 }
 
 static result_t*
 fossil_get_info(vccontext_t* context)
 {
     result_t* result = init_result();
-    char buf[2048];
     char *t;
-    FILE *stream;
     int tab_len = 14;
     char buf2[81];
 
     // enough to cover all the usual fields (note that 'comment:' can be
     // several lines long) plus eventual output indicating changes in
     // the repo.
-    if (!(stream = popen("fossil status", "r"))) {
-        debug("Unable to read output of 'fossil status'");
+    char *argv[] = {"fossil", "status", NULL};
+    capture_t *capture = capture_child("fossil", argv);
+    if (capture == NULL) {
+        debug("unable to execute 'fossil status'");
         return NULL;
     }
-
-    size_t rlen = fread(buf, sizeof(char), 2047, stream);
-    buf[rlen] = '\0';
-    pclose(stream);
+    char *cstdout = capture->stdout.buf;
 
     if (context->options->show_branch) {
-        if ((t = strstr(buf, "\ntags:"))) {
+        if ((t = strstr(cstdout, "\ntags:"))) {
             // This in fact shows also other tags than just the
             // propagating ones (=branches).  So either we show all
             // of them (as now), or we can show only the first one
             // (which should be the branch name); or we use one more
-            // 'system' call to read the output of 'fossil branch'.
+            // child process to read the output of 'fossil branch'.
             get_till_eol(buf2, t + tab_len + 1, 80);
             debug("found tag line: '%s'", buf2);
             result_set_branch(result, buf2);
         }
     }
     if (context->options->show_revision) {
-        if ((t = strstr(buf, "\ncheckout:"))) {
+        if ((t = strstr(cstdout, "\ncheckout:"))) {
             get_till_eol(buf2, t + tab_len + 1, 80);
             debug("found revision line: '%s'", buf2);
             result_set_revision(result, buf2, 12);
         }
     }
     if (context->options->show_modified) {
-        // This can be also done by 'test -n'ing 'fossil changes',
-        // but we save a system() call this way.
-        if ( strstr(buf, "\nEDITED") || strstr(buf, "\nADDED")
-            || strstr(buf, "\nDELETED") || strstr(buf, "\nMISSING")
-            || strstr(buf, "\nRENAMED") || strstr(buf, "\nNOT_A_FILE")
-            || strstr(buf, "\nUPDATED") || strstr(buf, "\nMERGED") )
-            result->modified = 1;
+        // This can be also done by checking if 'fossil changes'
+        // prints anything, but we save a child process this way.
+        result->modified = (strstr(cstdout, "\nEDITED") ||
+                            strstr(cstdout, "\nADDED") ||
+                            strstr(cstdout, "\nDELETED") ||
+                            strstr(cstdout, "\nMISSING") ||
+                            strstr(cstdout, "\nRENAMED") ||
+                            strstr(cstdout, "\nNOT_A_FILE") ||
+                            strstr(cstdout, "\nUPDATED") ||
+                            strstr(cstdout, "\nMERGED"));
     }
     if (context->options->show_unknown) {
         // This can't be read from 'fossil status' output
-        int status = system("test -n \"$(fossil extra)\"");
-        if (WEXITSTATUS(status) == 0)
-            result->unknown = 1;
+        char *argv[] = {"fossil", "extra", NULL};
+        capture = capture_child("fossil", argv);
+        if (capture == NULL) {
+            debug("unable to execute 'fossil extra'");
+            return NULL;
+        }
+        result->unknown = (capture->stdout.len > 0);
     }
 
     return result;
 #include <stdlib.h>
 #include <string.h>
 #include <sys/wait.h>
+
 #include "git.h"
+#include "capture.h"
+#include "common.h"
+
 
 static int
 git_probe(vccontext_t* context)
             }
         }
         if (context->options->show_modified) {
-            int status = system("git diff --no-ext-diff --quiet --exit-code");
-            if (WEXITSTATUS(status) == 1)       /* files modified */
-                result->modified = 1;
+            char *argv[] = {
+                "git", "diff", "--no-ext-diff", "--quiet", "--exit-code", NULL};
+            capture_t *capture = capture_child("git", argv);
+            result->modified = (capture->status == 1);
+
             /* any other outcome (including failure to fork/exec,
                failure to run git, or diff error): assume no
                modifications */
+            free_capture(capture);
         }
         if (context->options->show_unknown) {
-            int status = system("test -n \"$(git ls-files --others --exclude-standard)\"");
-            if (WEXITSTATUS(status) == 0)
-                result->unknown = 1;
+            char *argv[] = {
+                "git", "ls-files", "--others", "--exclude-standard", NULL};
+            capture_t *capture = capture_child("git", argv);
+            result->unknown = (capture != NULL && capture->stdout.len > 0);
+
             /* again, ignore other errors and assume no unknown files */
+            free_capture(capture);
         }
     }
 
 static size_t get_mq_patchname(char* str, const char* nodeid, size_t n)
 {
     char buf[1024];
-    char status_filename[] = ".hg/patches/status";
+    char status_filename[512] = ".hg/patches/status";
     static const char QQ_STATUS_FILE_PAT[] = ".hg/patches-%s/status";
     static const size_t MAX_QQ_NAME = sizeof(status_filename)
         - (sizeof(QQ_STATUS_FILE_PAT) - 2 - 1);  // - "%s" - '\0'

tests/setup-fossil

+#!/bin/sh
+
+# Setup the test fossil repository that will be used by test-fossil.
+#
+# Output is fossil-repo, which will be unpacked for each test run.
+
+. ./common.sh
+
+check_available "fossil version" "This is fossil"
+set -ex
+cd `dirname $0`
+rm -rf fossil-repo
+fossil init -A alice fossil-repo
+fossil user default alice -R fossil-repo
+rm -fr fossil-work
+mkdir fossil-work
+cd fossil-work
+fossil open ../fossil-repo
+
+echo a > a
+echo b > b
+fossil add a b
+fossil commit -m "add a, b"
+
+mkdir .fossil-settings
+cat > .fossil-settings/ignore-glob <<EOF
+~$
+\.o$
+EOF
+fossil add .fossil-settings/ignore-glob
+fossil commit -m "add ignore-glob"
+
+echo fix >> b
+fossil commit --branch stable -m "fix a bug"
+
+fossil checkout trunk
+
+cd ..
+rm -rf fossil-work

tests/test-fossil

+#!/bin/sh
+
+# Tests that require a real fossil repository and executable.
+
+. ./common.sh
+
+check_fossil()
+{
+    check_available \
+    "fossil version" \
+    "This is fossil" \
+    "fossil not found: skipping this test script"
+}
+
+find_fossilrepo()
+{
+    fossilrepo="$testdir/fossil-repo"
+    if [ ! -f $fossilrepo ]; then
+    echo "$fossilrepo not found" >&2
+    exit 1
+    fi
+}
+
+pretest()
+{
+    [ -d "$tmpdir" ] || die "tmpdir ($tmpdir) does not exist"
+    cd $tmpdir
+    rm -rf fossil-work
+    mkdir fossil-work
+    cd fossil-work
+    fossil open $fossilrepo > /dev/null
+}
+
+posttest()
+{
+    :
+}
+
+# default prompt format in test repo
+test_basics()
+{
+    pretest
+    assert_vcprompt "show branch 1" "trunk" "%b"
+    fossil checkout stable > /dev/null
+    assert_vcprompt "show branch 2" "stable" "%b"
+
+    # not implemented yet
+    echo foo >> b
+    echo junk > junk
+    assert_vcprompt "show modified" "+" "%m"
+    assert_vcprompt "show unknown" "?" "%u"
+
+    posttest
+}
+
+check_fossil
+find_vcprompt
+find_fossilrepo
+setup
+
+test_basics
+
+report

tests/test-simple

     assert_vcprompt "git subdir" "foo"
 }
 
+test_simple_fossil()
+{
+    cd $tmpdir
+    mkdir fossil && cd fossil
+
+    touch _FOSSIL_
+    assert_vcprompt "fossil broken" "fossil:(unknown)" "%n:%b"
+
+    rm _FOSSIL_
+    assert_vcprompt "fossil norepo" "" "%n:%b"
+    assert_vcprompt "fossil norepo" "" "%n:%b"
+
+    touch .fslckout
+    assert_vcprompt "fossil broken" "fossil:(unknown)" "%n:%b"
+}
+
 test_simple_hg()
 {
     cd $tmpdir
     mkdir .hg/patches-foo
     printf 'a123456789abcdefghij\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0' \
         > .hg/dirstate
-    echo '613132333435363738396162636465666768696a:baz.diff' >> .hg/patches-foo/status
+    echo '613132333435363738396162636465666768696a:qux.diff' >> .hg/patches-foo/status
     echo 'foo' > .hg/patches.queue  # named queue
-    assert_vcprompt "hg_mq applied named mq" "hg:613132333435[baz.diff]/foo" "%n:%r/%b"
+    assert_vcprompt "hg_mq applied named mq" "hg:613132333435[qux.diff]/foo" "%n:%r/%b"
 }
 
 test_simple_hg_revlog ()
 test_no_vc
 test_root
 test_simple_cvs
+test_simple_fossil
 test_simple_git
 test_simple_hg
 test_simple_hg_bookmarks