#!/bin/sh
# Regression test for winetricks

# TODO:
# add selfupdate tests
# automate non -q verbs with autohotkey

# Override this if you want to put the work area on a different disk
WINE_PREFIXES=${WINE_PREFIXES:-$HOME/winetrickstest-prefixes}
XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
cache="$XDG_CACHE_HOME/winetricks"

set -x

# Disable winetricks update checks:
WINETRICKS_LATEST_VERSION_CHECK=disabled
export WINETRICKS_LATEST_VERSION_CHECK

# Disable some verbose output that makes parsing harder:
WINETRICKS_SUPER_QUIET=1
export WINETRICKS_SUPER_QUIET

# verbs known to not work in -q mode yet
BLACKLIST="kde|mingw|psdk2003|psdkwin7"
# verbs that hang in -q because of simple problem we should work around soon
BLACKLIST="$BLACKLIST|vc2005trial"
# verbs that are too slow
BLACKLIST="$BLACKLIST|bfbc2|dxsdk_nov2006|dxsdk_jun2010"
# verbs that are too flaky
BLACKLIST="$BLACKLIST|wowtrial|mfsxde"
# steam verbs I don't have
BLACKLIST="$BLACKLIST|borderlands|supermeatboy|trine"
# broken http://bugs.winehq.org/show_bug.cgi?id=26411:
BLACKLIST="$BLACKLIST|mfsxde|mfsx_demo"
# hang lately, probably ui changes in install process
BLACKLIST="$BLACKLIST|bioshock2|audibledm"
# We don't have -q support for these
BLACKLIST="$BLACKLIST|dx8sdk"
# broken/flaky, http://bugs.winehq.org/show_bug.cgi?id=26016
BLACKLIST="$BLACKLIST|xmllite"
# redundant metaverbs
BLACKLIST="$BLACKLIST|allcodecs"
# doesn't always quit properly:
BLACKLIST="$BLACKLIST|spotify"
# FIXME: (wine/winetricks bug?) usually hangs in winetricks-test:
BLACKLIST="$BLACKLIST|dotnet30sp1|dotnet35|dotnet35sp1|dotnet45"
# mfplat.dll.MFTRegister https://bugs.winehq.org/show_bug.cgi?id=37811
BLACKLIST="$BLACKLIST|xvid"
# https://bugs.winehq.org/show_bug.cgi?id=40460
BLACKLIST="$BLACKLIST|dotnet20sp2|xna31"

# Travis CI is apparently blocking archive.org, which breaks several verbs:
# https://github.com/travis-ci/travis-ci/issues/6798
# Blacklist those verbs on those hosts:
# FIXME: setup another test target for torify, use it to d/l these?
ARCHIVE_ORG_BLACKLIST="atmlib|avifil32|cabinet|cmd|comctl32ocx|comdgl32ocx|gmdls|mdac27|mfc40|msflxgrd|mshflxgd"
ARCHIVE_ORG_BLACKLIST="$ARCHIVE_ORG_BLACKLIST|msls31ms|msmask|msscript|msxml3|pdh|pngfilt|riched30"
ARCHIVE_ORG_BLACKLIST="$ARCHIVE_ORG_BLACKLIST|richtx32|secur32|tabctl32|usp10|"

# See https://docs.travis-ci.com/user/environment-variables/#Default-Environment-Variables
if [ "$TRAVIS" = "true" ] ; then
    BLACKLIST="$ARCHIVE_ORG_BLACKLIST|$BLACKLIST"
fi

# Tests that fail under Xvfb
XVFB_DOTNET_BLACKLIST="dotnet11|dotnet11sp1|dotnet20|dotnet20sdk|dotnet20sp1|dotnet30|dotnet40|dotnet46"
XVFB_BLACKLIST="$XVFB_DOTNET_BLACKLIST|adobeair|binkw32|dirac|directmusic|dxdiag|flash|gdiplus_winxp|gfw|ie6|ie7|ie8"
XVFB_BLACKLIST="$XVFB_BLACKLIST|jet40|nuget|quicktime72|vcrun2008|vcrun2010|vcrun2012|vcrun2013|vcrun2015"
XVFB_BLACKLIST="$XVFB_BLACKLIST|vjrun20|windowscodecs|wmi|wmp9|wmp10|wsh56js|wsh56vb|xmllite|xna31|xna40|xvid"

# Verbs known to update frequently
QUICKCHECK="flash steam"

passes=0
errors=0

# Check for programs this script (or winetricks) uses.
# Better to find out they're missing now than in the
# middle of a two day run.
check_deps() {
    for tool in time cabextract
    do
        which $tool
        ret=$?

        if [ ! $ret -eq 0 ]
        then
            echo "Please install $tool."
            exit 1
        fi
     done
}

fail()
{
    echo "FAIL: $*"
    errors=$((errors + 1))
}

pass()
{
    echo "PASS: $*"
    passes=$((passes + 1))
}

w_die()
{
    echo "$*"

    exit 1
}

w_time()
{
    # OSX time doesn't support -o, so try it first:
    if ! /usr/bin/time -o test.log echo test > /dev/null 2>&1
    then
        /usr/bin/time -p "$@"
    else
        /usr/bin/time -p -o time.log "$@"
    fi
}

case "$LANG" in
    ""|"C") echo "Some games won\'t install in the Posix locale; doing \'export LANG=en_US.UTF-8\'" ; export LANG=en_US.UTF-8;;
esac

case "$OS" in
 "Windows_NT")
    # Mostly unimplemented...
    # Cheezy fix for getting rid of double slashes when running cygwin in wine
    case "$HOME" in
      /) HOME="" ;;
    esac
    WINE=""
    WINESERVER=true
    DRIVE_C="C:/"
    ;;
 *)
    export WINE=${WINE:-wine}
    # Find wineserver.  Some distros (Debian) don't have it on the path,
    # on the mistaken understanding that user scripts never need it :-(
    # If wineserver is from wine-development set WINE to wine-development.
    # FIXME: get packagers to put wineserver on the path.
    for x in \
        "$WINESERVER" \
        "${WINE}server" \
        "$(which wineserver 2> /dev/null)" \
        "$(dirname $WINE)/server/wineserver" \
        /usr/lib/wine/wineserver \
        /usr/lib/i386-kfreebsd-gnu/wine/wineserver \
        /usr/lib/i386-linux-gnu/wine/wineserver \
        /usr/lib/powerpc-linux-gnu/wine/wineserver \
        /usr/lib/i386-kfreebsd-gnu/wine/bin/wineserver \
        /usr/lib/i386-linux-gnu/wine/bin/wineserver \
        /usr/lib/powerpc-linux-gnu/wine/bin/wineserver \
        /usr/lib/x86_64-linux-gnu/wine/bin/wineserver \
        /usr/lib/i386-kfreebsd-gnu/wine-development/wineserver \
        /usr/lib/i386-linux-gnu/wine-development/wineserver \
        /usr/lib/powerpc-linux-gnu/wine-development/wineserver \
        /usr/lib/x86_64-linux-gnu/wine-development/wineserver \
        file-not-found
    do
        if [ -x "$x" ] ; then
            case "$x" in
                /usr/lib/*/wine-development/wineserver)
                        if [ -x /usr/bin/wine-development ] ; then WINE="/usr/bin/wine-development" ; fi ;;
            esac
            break
        fi
    done

    case "$x" in
        file-not-found) w_die "wineserver not found!" ;;
        *) WINESERVER="$x" ;;
    esac
    ;;
esac

srcdir=$(cd "$(dirname "$0")" || w_die "Could not cd to $(dirname "$0")" ; pwd)

test_speed()
{
    # shellcheck disable=SC2086
    if ! w_time $XVFB sh winetricks nocrashdialog "$1" > foo.log
    then
        fail "winetricks $1 returned status $?"
    fi

    # shellcheck disable=SC2046
    if [ $(wc -l < foo.log) -lt 5 ] ; then
        fail "winetricks $1 returned too few lines"
    fi

    if [ ! -f time.log ] ; then
        # OSX, fake it:
        seconds=0
    else
        seconds=$(awk '/real/ {print $2}' < time.log | sed 's/\..*//')
    fi

    echo "test_speed: winetricks $1 took $seconds seconds"
    # Longest runtime as of 11 Dec 2010 is 5 seconds on an e8400 with cygwin
    # shellcheck disable=SC2086
    if [ $seconds -gt 7 ] ; then
        fail "test_speed: winetricks $1 took $seconds seconds"
    fi
}

test_app_checksums()
{
    # Verify the installation
    if [ -f "$srcdir/winetricksverify.d/$app.sha1sum" ]
    then
        windir="$($WINE cmd /c echo "%windir%" | cut -c 4- | tr -d '\015')"
        progdir="$($WINE cmd /c echo "%ProgramFiles%" | cut -c 4- | tr -d '\015')"

        cd "$DRIVE_C" || w_die "Could not cd to $DRIVE_C"
        # Fix up the filenames, which can differ between Windows versions/Wine:
        # FIXME: we need a way to guarantee that all _original_ .sha1sums are the same.
        # Preferably generated under 32-bit wine, so that we don't need a really complex sed
        # substitution here...
        sed -e "s!Program\ Files/!/$progdir/!" -e "s!/windows/!/$windir/!" < "$srcdir/winetricksverify.d/$app.sha1sum" > "$app.sha1sum.tmp"
        if ! sha1sum -c "$app.sha1sum.tmp"
        then
            fail "test_app_checksum $app !"
        fi
        rm "$app.sha1sum.tmp"
        cd "$srcdir" || w_die "Could not cd into $srcdir"
    fi
}

# Return the number of blocks available in the system
total_df()
{
    df | grep -v /dev/loop | awk '/^\// { sum += $4 } END { print sum }'
}

# for single apps, wrapper around test_command:
test_app()
{
    app=$1

    # Watch transient disk space
    DF_START=$(total_df)
    if [ -d "$W_CACHE/$app" ] ; then
        DU_CACHE_START=$(du -s "$W_CACHE/$app" | awk '{print $1}')
    else
        DU_CACHE_START=0
    fi
    touch df-daemon
    (set +x; while test -f df-daemon; do total_df; sleep 1; done ) > df-during.log &

    test_command --verify "$app"
    test_app_checksums

    # Post install:
    # Don't check whether metaverbs are installed
    case "$app" in
    allcodecs) ;;
    *)
        # no xvfb needed
        sh winetricks -q list-installed > list-installed.out
        if ! grep -w "$app" list-installed.out
        then
            fail "test app $app not installed after install?"
        fi
        ;;
    esac

    # Cleanup..
    rm df-daemon
    # Total max disk usage = max df change plus any initial blocks in cache
    DF_MIN=$(awk '{ if (min == "" || $1 < min) min=$1; } END {printf "%d\n", min}' < df-during.log )
    DF_DIFF=$((DF_START - DF_MIN))
    TOTAL=$((DF_DIFF + DU_CACHE_START))
    echo "test_app: ${app}: max_disk $TOTAL blocks."
    TOTAL_MB=$((TOTAL / 1024))
    mkdir -p measurements
    echo "${app}:size_MB=${TOTAL_MB},time_sec=${seconds}" >> "measurements/$app.dat"
}

test_command()
{
    command="$*"
    _command_dash="$(echo "$command" | tr " " "-")"

    export WINEPREFIX="$WINE_PREFIXES/$_command_dash"
    DRIVE_C="$WINEPREFIX/dosdevices/c:"

    # always use a clean $WINEPREFIX
    if [ -d "$WINEPREFIX" ] ; then
        rm -rf "$WINEPREFIX"
    fi

    # Isolate us from the user's home directory
    $XVFB sh -x winetricks sandbox

    echo "Installing $command"

    # shellcheck disable=SC2086
    if [ "$EXPECT_FAIL" = "yes" ] ; then
        # A success is failure:
        # shellcheck disable=SC2086
        $XVFB sh winetricks --no-isolate -q nocrashdialog "$@" && fail "$command succeeded, should have failed"
    elif ! w_time $XVFB sh winetricks --no-isolate -q nocrashdialog "$@" ; then
        rm df-daemon
        fail "test_command $command failed!"
        return
    else
        echo "winetricks $* completed"
    fi

    if [ ! -f time.log ] ; then
        seconds=0
    else
        seconds=$(awk '/real/ {print $2}' < time.log | sed 's/\..*//')
    fi

    echo "test_app: ${app}: install_time $seconds seconds."

    # Cleanup:
    for x in $command
    do
        case $x in
        dotnet*)
            killall wineconsole ;;   # bug in wine?  the wineconsoles are for the langpacks, and seem empty.
        wmi)
            killall WinMgmt.exe ;;   # wmi starts a service
        fontxplorer)
            killall winefile.exe ;;  # a number of apps open a folder on the desktop
        hegemony*)
            killall Launcher.exe ;;  # or should this be in the verb?
        esac
    done
    killall notepad.exe          # Arcania-Gothic4 and others

    echo "Checking for dangling processes!"
    # shellcheck disable=SC2009
    ps augxw | grep \\.exe
    "$WINESERVER" -w
    echo "Wineserver done."

    pass "$@"
}

test_custom_verbs()
{
    # Custom .verb support isn't commonly used, and may break without notice for a while
    # Also try with --isolate:
    # https://github.com/Winetricks/winetricks/issues/599

    # Test as apps first, then dll, since they take different codepaths

    # First, a working 'app' as an app:
cat > true.verb <<_EOF
w_metadata true apps

load_true()
{
    echo "true should succeed"
    /bin/true
}

_EOF

    # Next, a broken 'app' as an app:
cat > false.verb <<_EOF
w_metadata false apps

load_false()
{
    echo "false should fail"
    false
}

_EOF

    # no xvfb needed
    sh winetricks --no-isolate true.verb ; ret=$?
    case $ret in
        0) pass "true.verb not isolated, as apps passed" ;;
        *) fail "true.verb not isolated, as apps failed" ;;
    esac

    # no xvfb needed
    sh winetricks --no-isolate false.verb ; ret=$?
    case $ret in
        0) fail "false.verb not isolated, as apps worked, should have failed" ;;
        1) pass "false.verb not isolated, as apps passed" ;;
        *) fail "false.verb not isolated, as apps failed in unexpected way" ;;
    esac

    # no xvfb needed
    sh winetricks --isolate true.verb ; ret=$?
    case $ret in
        0) pass "true.verb isolated, as apps passed" ;;
        *) fail "true.verb isolated, as apps failed" ;;
    esac

    # no xvfb needed
    sh winetricks --isolate false.verb ; ret=$?
    case $ret in
        0) fail "false.verb isolated, as apps worked, should have failed" ;;
        1) pass "false.verb isolated, as apps passed" ;;
        *) fail "false.verb isolated, as apps failed in unexpected way" ;;
    esac

    # Repeat as dll:

    # First, a working 'app' as a dll:
cat > true.verb <<_EOF
w_metadata true dlls

load_true()
{
    echo "true should succeed"
    true
}
_EOF

    # Next, a broken 'app' as a dll:
cat > false.verb <<_EOF
w_metadata false dlls

load_false()
{
    echo "false should fail"
    false
}
_EOF

    # no xvfb needed
    sh winetricks --no-isolate true.verb ; ret=$?
    case $ret in
        0) pass "true.verb isolated, as dlls passed" ;;
        *) fail "true.verb isolated, as dlls failed" ;;
    esac

    # no xvfb needed
    sh winetricks --no-isolate false.verb ; ret=$?
    case $ret in
        0) fail "false.verb isolated, as dlls worked, should have failed" ;;
        1) pass "false.verb isolated, as dlls passed" ;;
        *) fail "false.verb isolated, as dlls failed in unexpected way" ;;
    esac

    # no xvfb needed
    sh winetricks --no-isolate true.verb ; ret=$?
    case $ret in
        0) pass "true.verb isolated, as dlls passed" ;;
        *) fail "true.verb isolated, as dlls failed" ;;
    esac

    # no xvfb needed
    sh winetricks --no-isolate false.verb ; ret=$?
    case $ret in
        0) fail "false.verb isolated, as dlls worked, should have failed" ;;
        1) pass "false.verb isolated, as dlls passed" ;;
        *) fail "false.verb isolated, as dlls failed in unexpected way" ;;
    esac

    rm false.verb true.verb
    pass
}

test_dlls()
{
    # no xvfb needed
    sh winetricks list-manual-download > manual.log
    sh winetricks dlls list | awk '{print $1}' > dlls.log
    if grep .------------------- dlls.log ; then
        fail "output of dlls list contained garbage"
        exit 1
    fi

    sort -u < dlls.log | fgrep -w -v -f manual.log | egrep -v "$BLACKLIST" > dlls.verbs

    if [ ! -f dlls.verbs ] ; then
        w_die "dlls.verbs doesn't exist? No verbs to test!"
    elif [ -f dlls.verbs ] && [ ! -s dlls.verbs ] ; then
        w_die "dlls.verbs exists, but it is empty"
    else
        echo "Testing these verbs:"
        cat dll.verbs
    fi

    while IFS= read -r line; do
        test_app "$line"
    done < dlls.verbs
}

test_dotnet()
{
    # verify that each individual installer works:
    # skipping dotnet30sp1/dotnet35/dotnet35sp1/dotnet45, known to hang
    # FIXME: dotnet20sp2: https://bugs.winehq.org/show_bug.cgi?id=40460
    for x in dotnet11 dotnet11sp1 dotnet20 dotnet20sp1 dotnet30 dotnet40 dotnet46
    do
        echo "testing $x"
        test_command --verify $x
    done

    # FIXME: add other possible combinations

    # combinations that should work:
    # FIXME: dotnet20sp2: https://bugs.winehq.org/show_bug.cgi?id=40460
    for combo in "dotnet20 dotnet20sp2"
    do
        # shellcheck disable=SC2086
        EXPECT_FAIL=yes test_command --verify $combo
    done

    # combinations that should break:
    for fail_combo in "dotnet11 dotnet20"
    do
        # shellcheck disable=SC2086
        EXPECT_FAIL=yes test_command $fail_combo
    done
}

test_manual_dlls()
{
    # no xvfb needed
    sh winetricks list-manual-download > manual.log
    sh winetricks dlls list | awk '{print $1}' > dlls.log
    if grep .------------------- dlls.log ; then
        fail "output of dlls list contained garbage"
        exit 1
    fi
    cat dlls.log manual.log | sort | uniq -c | awk '$1 == 2 {print $2}' | egrep -v "$BLACKLIST" > dlls.verbs

    while IFS= read -r line; do
        test_app "$line"
    done < dlls.verbs
}

test_install_cached_or_download()
{
    # no xvfb needed
    sh winetricks list-cached list-download > ticd.log
    if grep .------------------- ticd.log ; then
        fail "output of list-cached list-download contained garbage"
        exit 1
    fi
    sort -u < ticd.log | egrep -v "$BLACKLIST" > ticd.verbs

    if true ; then
        # Split into two, only do half
        nverbs=$(wc -l < ticd.verbs )
        firsthalf=$((nverbs / 2 + 1))
        sed "${firsthalf}"',$d' < ticd.verbs > ticd-1.verbs
        sed '1,'"${firsthalf}"d   < ticd.verbs > ticd-2.verbs
        VERBS=$(cat ticd-1.verbs)
    else
        VERBS=$(cat ticd.verbs)
    fi

    for a in $VERBS
    do
        test_app "$a"
    done

    # no xvfb needed
    sh winetricks list-cached | sort > cached.txt

    # Verbs that are just wrappers around others don't detect cache/install state yet.
    # Verbs that are just informative placeholders don't ever download (gecko).
    # And some verbs (gecko110) don't usually install for other reasons.
    BLACKLIST_CACHE="allfonts\|cjkfonts\|allcodecs\|gecko\|gecko110\|fontfix"

    egrep -vw "$BLACKLIST_CACHE" ticd.verbs > download.txt
    comm -23 download.txt cached.txt > download-but-not-cached.txt
    echo "Supposedly downloadable"
    cat download.txt
    echo "Cached"
    cat cached.txt
    echo "Downloadable but not cached"
    cat download-but-not-cached.txt

    # shellcheck disable=SC2046
    if [ $(wc -l < download-but-not-cached.txt) != 0 ] ; then
        fail "test_install_cached_or_download: asked to install all downloadable apps, but some not listed as cached afterwards"
        cat download-but-not-cached.txt
    fi
}

test_quick()
{
    echo "warning: quick test takes up around 20GB"
    # Test the frequent download-changes-checksum offenders first
    export W_CACHE="$cache"
    for a in $QUICKCHECK
    do
        # shellcheck disable=SC2115
        rm -rf "$WINE_PREFIXES/$a"
        test_app "$a"
    done

    # And test all the automatically-downloadable dlls
    test_dlls
}

test_full() {
    test_quick
    test_dotnet
    test_speed list
    test_speed list-download
    #test_install_cached_or_download
    test_speed list-cached
    test_speed list-installed
    test_custom_verbs
}

test_xvfb() {
    if [ ! "$(which xvfb-run 2>/dev/null)" ] ; then
        w_die "Please install xvfb-run for xvfb tests"
    fi

    BLACKLIST="$BLACKLIST|$XVFB_BLACKLIST"
    export BLACKLIST

    # Not test_quick() since flash fails without proper X, but test_quick() doesn't respect $BLACKLIST
    # Also, we don't really want to duplicate those tests twice, as this is for TravisCI where time is limited..
    test_dlls
}

case "$1" in
    check-deps) check_deps ; exit $? ;;
    custom-verbs) test_custom_verbs ;;
    dotnet) check_deps && test_dotnet ;;
    full)  check_deps && test_full ;;
    quick) check_deps && test_quick ;;
    xvfb-check) check_deps && XVFB=xvfb-run && export XVFB && test_xvfb ;;
    *) echo "Usage: $0 quick\|full" ; exit 1 ;;
esac

echo "Test over, $errors failures, $passes successes."
if [ $errors = 0 ] && [ $passes -gt 0 ] ; then
    echo PASS
    exit 0
else
    echo FAIL
    exit 1
fi
