#!/bin/sh
# DaliApplication / DaliGadget on-device integration tests.
#
# Runs on the device, as root, from the dali-application-integration-tests
# package:
#
#   dali-integration-test setup              register the test packages
#   dali-integration-test list               list the cases
#   dali-integration-test [run] [case...]    run cases (default: all)
#   dali-integration-test record [case...]   record the lifecycle sequences of
#                                            the ui_app_main() reference
#
# Case names starting with "lc_" are lifecycle cases: the DaliApplication test
# application must log the same events, in the same order, as the reference
# application recorded in expected/. The others are DaliGadget cases: the host
# logs "RESULT <case> PASS" and the gadgets log the lines checked in
# check_gadget_side (see expect_order); a crash or timeout fails a case.
DATADIR=/usr/share/dali-application/integration-test
EXPECTED=${EXPECTED_DIR:-$DATADIR/expected}

TAG=LIFECYCLE_TEST
APP=org.tizen.dali-lifecycle-test
OTHER=org.tizen.dali-lifecycle-test.other
REFERENCE=org.tizen.dali-lifecycle-reference
MODE_FILE=/tmp/dali-lifecycle-test.mode
GADGET_HOST=org.tizen.dali-gadget-test

PACKAGES="org.tizen.dali-lifecycle-test org.tizen.dali-lifecycle-reference
$GADGET_HOST org.tizen.dali-gadget-test-main org.tizen.dali-gadget-test-empty
org.tizen.dali-gadget-test-ver1 org.tizen.dali-gadget-test-ver2"

GADGET_CASES="basic finish_in_message remove_in_listener remove_in_created
finish_in_create remove_in_resume remove_in_pause
remove_in_created_two_listeners remove_in_message
pause_in_pause resume_in_resume
listener_removed_in_listener finish_in_dispatch
finish_in_broadcast dtor_pause empty_view routing pkg_select multi_class
remove_unparent refresh metadata locale locale_live"
LIFECYCLE_CASES="lc_start lc_start_terminate lc_start_data lc_relaunch
lc_background lc_foreground lc_background_terminate
lc_relaunch_in_background lc_pause_twice lc_resume_twice
lc_exit_in_control lc_exit_in_create lc_fail_create lc_exit_in_resume
lc_exit_in_pause lc_exit_in_terminate
lc_language lc_region lc_timezone lc_low_memory lc_low_battery
lc_event_background lc_event_in_create"
# Cases only a DaliApplication can run, because they use DALi: there is no
# reference run to record, so each has its own check (lc_check).
DALI_ONLY_CASES="lc_terminate_dali"
ALL_CASES="$LIFECYCLE_CASES $DALI_ONLY_CASES $GADGET_CASES"

# $1: log file, rest: patterns that must appear in this order.
expect_order() {
  local log=$1 last=0 line pattern; shift
  for pattern in "$@"; do
    line=$(grep -n -F -- "$pattern" "$log" | awk -F: -v after=$last \
           '$1 > after { print $1; exit }')
    if [ -z "$line" ]; then
      echo "    missing (in order): $pattern"; return 1
    fi
    last=$line
  done
}

# $1: log file, $2: pattern. The pattern must be logged exactly once.
expect_once() {
  local n
  n=$(grep -cF "$2" "$1")
  [ "$n" -eq 1 ] || { echo "    '$2' logged $n times, expected 1"; return 1; }
}

# $1: log file, $2: case. Gadget-side ordering per case.
check_gadget_side() {
  local log=$1
  # No scenario may see its own object freed under a running callback.
  if grep -q "destroyed_before_return=1" "$log"; then
    echo "    callback ran on a freed gadget"; return 1
  fi
  case $2 in
    basic) expect_order "$log" "OnCreate" "OnResume" "OnPause" \
             "OnMessageReceived cmd=echo" "OnDestroy" "~dtor" ;;
    finish_in_message) expect_order "$log" "OnResume" \
             "finish-from-message destroyed_before_return=0" "OnPause" \
             "OnDestroy" "~dtor" ;;
    remove_in_listener) expect_order "$log" "OnResume" \
             "OnMessageReceived cmd=ping_host" \
             "removing sender inside the message listener" "OnPause" \
             "OnDestroy" "~dtor" ;;
    remove_in_created) expect_order "$log" "OnCreate" \
             "removing the gadget inside the Created notification" \
             "OnDestroy" "~dtor" ;;
    remove_in_resume) expect_order "$log" "[main/RemoveInResumeGadget] OnResume" \
             "OnPause" "OnDestroy" "~dtor" ;;
    remove_in_message) expect_order "$log" \
             "[main/RemoveInMessageGadget] OnMessageReceived" \
             "remove-from-message destroyed_before_return=0" "OnPause" \
             "OnDestroy" "~dtor" ;;
    remove_in_created_two_listeners) expect_order "$log" "OnCreate" \
             "removing the gadget inside the Created notification" \
             "OnDestroy" "~dtor" ;;
    remove_in_pause) expect_order "$log" "[main/RemoveInPauseGadget] OnPause" \
             "OnDestroy" "~dtor" &&
             expect_once "$log" "[main/RemoveInPauseGadget] OnPause" ;;
    pause_in_pause) expect_once "$log" "[main/PauseInPauseGadget] OnPause" ;;
    resume_in_resume) expect_once "$log" "[main/ResumeInResumeGadget] OnResume" ;;
    finish_in_create) expect_order "$log" "[main/FinishInCreateGadget] OnCreate" \
             "OnDestroy" "~dtor" ;;
    finish_in_dispatch) expect_order "$log" "OnAppControlReceived" \
             "finish-from-appcontrol destroyed_before_return=0" \
             "DispatchAppControl returned" "OnDestroy" ;;
    finish_in_broadcast) expect_order "$log" "OnLanguageChanged ko_KR.UTF-8" \
             "finish-from-broadcast destroyed_before_return=0" \
             "BroadcastLanguageChanged returned" "OnDestroy" ;;
    dtor_pause) expect_order "$log" "OnResume" \
             "destroying manager with a Resumed gadget" "OnPause" \
             "OnDestroy" ;;
    empty_view) expect_order "$log" "OnCreate (returns empty view)" \
             "OnDestroy" ;;
    routing) expect_order "$log" "[ver2/MainGadget] OnAppControlReceived" &&
             ! grep -q "\[main/MainGadget\] OnAppControlReceived" "$log" ;;
    pkg_select) expect_order "$log" "[ver2/MainGadget] OnCreate" ;;
    multi_class) expect_order "$log" "[main/SecondGadget] OnCreate" ;;
    *) ;;
  esac
}

# --- lifecycle helpers -------------------------------------------------------

# One phase of a scenario is what the application logged between two steps. It
# is written "life: create control resume | sys: orientation=0".
#
#   life  the lifecycle callbacks, in the order they ran. The order is a rule.
#   sys   the system events, sorted. ui_app_main() does not order them against
#         each other or against the lifecycle callbacks of the same phase (a
#         pause and the suspended event that follows it switch places between
#         runs), so only which ones came is compared.
#
# The orientation event the platform sends at the start, and the suspended event
# that follows a pause, are dropped from a phase that ends in terminate: an
# application that exits meanwhile may or may not get them, in ui_app_main() as
# well.
phase_of() {
  local events life sys
  events=$(dlogutil -d $TAG 2>/dev/null | sed -n "s/^.*\[$KIND\] //p")
  life=$(echo "$events" | grep -E '^(create|control|resume|pause|terminate)(=|$)' | tr '\n' ' ' | sed 's/ $//')
  sys=$(echo "$events" | grep -v -E '^(create|control|resume|pause|terminate)(=|$)' | grep -v '^$')
  case " $life " in
    *" terminate "*) sys=$(echo "$sys" | grep -v -E '^(orientation|suspended)=') ;;
  esac
  sys=$(echo "$sys" | sort | tr '\n' ' ' | sed 's/ $//')
  echo "life: $life | sys:${sys:+ $sys}"
}

log_lines() {
  dlogutil -d $TAG 2>/dev/null | wc -l | tr -d ' '
}

# Waits until the log has stopped growing for $1 seconds (at most $2).
settle() {
  local quiet=${1:-3} limit=${2:-25} prev=-1 cur stable=0 n=0
  while [ $n -lt $limit ]; do
    sleep 1; n=$((n + 1))
    cur=$(log_lines)
    if [ "$cur" = "$prev" ]; then stable=$((stable + 1)); else stable=0; fi
    prev=$cur
    [ $stable -ge $quiet ] && return 0
  done
}

# Runs "$@", waits for the log to react (at most 8 s), then to settle, and
# records what the application logged as one phase.
step() {
  local before n=0
  before=$(log_lines)
  "$@" || STEP_FAILED=1
  while [ $n -lt 8 ] && [ "$(log_lines)" = "$before" ]; do sleep 1; n=$((n + 1)); done
  settle
  phase_end
}

# Records what the application logged since the last phase and clears the log.
phase_end() {
  PHASES="$PHASES$(phase_of)
"
  dlogutil -c
}

is_running() {
  app_launcher -r "$1" 2>&1 | grep -q "is running"
}

stop_app() {
  local n=0
  app_launcher -t "$1" >/dev/null 2>&1
  while [ $n -lt 10 ] && is_running "$1"; do sleep 1; n=$((n + 1)); done
  is_running "$1" && app_launcher -k "$1" >/dev/null 2>&1
  return 0
}

vc_get() {
  vconftool get "$1" 2>/dev/null | sed -n 's/^.*value = \(.*\) (\(String\|Int32\|Bool\|Double\))$/\1/p'
}

# $1 type (string|int), $2 key, $3 value. A failing set would leave a case
# looking at an event that never happened, so it is reported.
vc_set() {
  local out
  out=$(vconftool set -f -t "$1" "$2" "$3" 2>&1) || true
  if [ "$(vc_get "$2")" != "$3" ]; then
    echo "    could not set $2 to $3: $out" >&2
    return 1
  fi
}

# Scenario environment. $UT is the application whose events are checked.
UT=$APP
KIND=dali
VC_RESTORE=""
PHASES=""
STEP_FAILED=0

lc_setup() {
  stop_app $APP; stop_app $OTHER; stop_app $REFERENCE
  rm -f $MODE_FILE
  VC_RESTORE=""
  PHASES=""
  STEP_FAILED=0
  dlogutil -c
}

# The applications may not read a file the shell created (Smack): give it the
# floor label.
set_mode() {
  echo "$1" > $MODE_FILE
  chsmack -a '_' $MODE_FILE
}

lc_teardown() {
  local item
  stop_app $APP; stop_app $OTHER; stop_app $REFERENCE
  rm -f $MODE_FILE
  # Put the system values a case changed back (not part of its sequence).
  for item in $VC_RESTORE; do
    vc_set "${item%%|*}" "$(echo "$item" | cut -d'|' -f2)" "$(echo "$item" | cut -d'|' -f3-)"
  done
  VC_RESTORE=""
}

# Sets a vconf key, remembering its value for lc_teardown. $1 type, $2 key,
# $3 value, $4 "quiet" to set it before the application runs.
vc_change() {
  # Only the first change of a key records the value to restore.
  case "$VC_RESTORE" in
    *"|$2|"*) ;;
    *) VC_RESTORE="$VC_RESTORE $1|$2|$(vc_get $2)" ;;
  esac
  if [ "$4" = quiet ]; then vc_set "$1" "$2" "$3"; else step vc_set "$1" "$2" "$3"; fi
}

# Crash dumps the platform kept for the test applications.
crash_count() {
  ls /opt/usr/share/crash/dump 2>/dev/null | grep -c 'dali-lifecycle-test'
}

app_start() { app_launcher -s $UT "$@" >/dev/null 2>&1; }
app_stop() { app_launcher -t $UT >/dev/null 2>&1; }
# Another application on top does not pause this one here, so the pause and
# resume requests that amd sends to the window manager's clients are used.
# aul_test exits with the process id for a resume, so its output tells whether
# the request was made.
app_pause() { aul_test pause $UT 2>&1 | grep -q "test successful"; }
app_resume() { aul_test resume $UT 2>&1 | grep -q "test successful"; }

# Scenario steps for lifecycle case $1. Each leaves the events in the log.
lc_steps() {
  case $1 in
    lc_start) step app_start ;;
    lc_start_terminate) step app_start; step app_stop ;;
    lc_start_data) step app_start cmd hello ;;
    lc_relaunch) step app_start; step app_start cmd again ;;
    lc_background) step app_start; step app_pause ;;
    lc_foreground) step app_start; step app_pause; step app_resume ;;
    lc_background_terminate) step app_start; step app_pause; step app_stop ;;
    lc_relaunch_in_background) step app_start; step app_pause; step app_start cmd back ;;
    lc_pause_twice) step app_start; step app_pause; step app_pause ;;
    lc_resume_twice) step app_start; step app_resume ;;
    lc_exit_in_control) step app_start cmd exit ;;
    lc_exit_in_create) set_mode exit_in_create; step app_start ;;
    lc_fail_create) set_mode fail_create; step app_start ;;
    lc_exit_in_resume) set_mode exit_in_resume; step app_start ;;
    lc_exit_in_pause) set_mode exit_in_pause; step app_start; step app_pause ;;
    lc_exit_in_terminate) set_mode exit_in_terminate; step app_start; step app_stop ;;
    lc_terminate_dali)
      CRASHES=$(crash_count)
      set_mode dali_in_terminate; step app_start; step app_stop
      if [ "$(crash_count)" != "$CRASHES" ]; then
        echo "    the application crashed while terminating"; STEP_FAILED=1
      fi ;;
    lc_language)
      vc_change string db/menu_widget/language en_US.UTF-8 quiet
      step app_start; vc_change string db/menu_widget/language ko_KR.UTF-8 ;;
    lc_region)
      vc_change string db/menu_widget/regionformat en_US.UTF-8 quiet
      step app_start; vc_change string db/menu_widget/regionformat ko_KR.UTF-8 ;;
    lc_timezone)
      vc_change string db/setting/timezone_id Asia/Seoul quiet
      step app_start; vc_change string db/setting/timezone_id America/New_York ;;
    lc_low_memory)
      step app_start; vc_change int memory/sysman/low_memory 2
      vc_set int memory/sysman/low_memory 1; settle ;;
    lc_low_battery)
      step app_start; vc_change int memory/sysman/battery_status_low 2
      vc_set int memory/sysman/battery_status_low 4; settle ;;
    lc_event_background)
      vc_change string db/menu_widget/language en_US.UTF-8 quiet
      step app_start; step app_pause
      vc_change string db/menu_widget/language ko_KR.UTF-8 ;;
    lc_event_in_create)
      set_mode slow_create
      vc_change string db/menu_widget/language en_US.UTF-8 quiet
      app_start; sleep 1; vc_set string db/menu_widget/language ko_KR.UTF-8 || STEP_FAILED=1
      settle 4 30; phase_end ;;
    *) echo "    unknown lifecycle case"; return 1 ;;
  esac
}

# Runs the steps of case $1 once, writing the phases to $2 (a file: a failing
# step must reach the caller, which a command substitution would hide). Returns
# the status of the steps.
lc_once() {
  local rc
  lc_setup
  lc_steps "$1"
  rc=$?
  [ $STEP_FAILED -eq 0 ] || rc=1
  printf '%s' "$PHASES" > "$2"
  lc_teardown
  return $rc
}

# True when the files hold the same text (the device has no cmp or diff).
same_file() {
  [ "$(cat "$1")" = "$(cat "$2")" ]
}

# Every phase must show the events the case is about; a case that changed
# nothing (a vconf key that did not take, a mode file that was not read) would
# otherwise pass by being consistently empty.
lc_sanity() {
  case $1 in
    lc_language) grep -q 'sys: lang=' "$2" ;;
    lc_region) grep -q 'sys: region=' "$2" ;;
    lc_timezone) grep -q 'sys: timezone=' "$2" ;;
    lc_low_memory) grep -q 'sys: lowmemory=' "$2" ;;
    lc_low_battery) grep -q 'sys: lowbattery=' "$2" ;;
    lc_event_background) grep -q 'sys: .*lang=' "$2" ;;
    lc_exit_in_create) grep -q 'mode=exit_in_create' "$2" ;;
    lc_fail_create) grep -q 'mode=fail_create' "$2" ;;
    lc_exit_in_resume) grep -q 'mode=exit_in_resume' "$2" ;;
    lc_exit_in_pause) grep -q 'mode=exit_in_pause' "$2" ;;
    lc_exit_in_terminate) grep -q 'mode=exit_in_terminate' "$2" ;;
    lc_exit_in_control) grep -q 'control=exit' "$2" ;;
    lc_event_in_create) grep -q 'mode=slow_create' "$2" ;;
    *) true ;;
  esac
}

# The check of a case that has no reference run: $1 case, $2 phases file.
lc_check() {
  case $1 in
    lc_terminate_dali)
      # OnTerminate() runs after DALi has been told to terminate, but DALi
      # still works: a window handle kept from OnCreate() is valid, and views
      # can be created, added to it and released. GetDefaultWindow() is empty
      # by then, so what an application releases in OnTerminate() has to be
      # held from OnCreate(). If a change makes GetDefaultWindow() valid here,
      # update the case.
      local step
      for step in 'mode=dali_in_terminate' 'terminate-held-window=valid' \
                  'terminate-window=empty' 'terminate-size=' \
                  'terminate-view-created' 'terminate-view-added' \
                  'terminate-view-removed' 'terminate-released'; do
        if ! grep -q -F -- "$step" "$2"; then
          echo "    OnTerminate() did not get as far as: $step"
          sed 's/^/      /' "$2"
          return 1
        fi
      done ;;
  esac
}

# $1: case, $2: "record" to write the sequence of the reference application.
# Recording runs the case $RECORD_RUNS times: a sequence is only written when
# every run produced the same one, because timing differences between runs
# have given a misleading order before.
run_lifecycle_case() {
  local c=$1 file="$EXPECTED/$1.seq" out=/tmp/dali-integration-test.lc.$$ run=1 rc
  if [ "$2" = record ]; then UT=$REFERENCE; KIND=ref; else UT=$APP; KIND=dali; fi

  case " $DALI_ONLY_CASES " in
    *" $c "*)
      UT=$APP; KIND=dali
      lc_once "$c" "$out"; rc=$?
      [ $rc -eq 0 ] && lc_check "$c" "$out"; rc=$?
      rm -f "$out"; return $rc ;;
  esac

  lc_once "$c" "$out"; rc=$?
  if [ $rc -ne 0 ]; then rm -f "$out"; return 1; fi
  if ! lc_sanity "$c" "$out"; then
    echo "    the case did not produce the events it is about:"; sed 's/^/      /' "$out"
    rm -f "$out"; return 1
  fi

  if [ "$2" = record ]; then
    while [ $run -lt ${RECORD_RUNS:-3} ]; do
      lc_once "$c" "$out.next"; rc=$?
      if [ $rc -ne 0 ] || ! same_file "$out" "$out.next"; then
        echo "    runs differ, not recorded:"
        sed 's/^/      run 1: /' "$out"; sed "s/^/      run $((run + 1)): /" "$out.next"
        rm -f "$out" "$out.next"; return 1
      fi
      run=$((run + 1))
    done
    mkdir -p "$EXPECTED"; cp "$out" "$file"
    sed 's/^/    /' "$out"; rm -f "$out" "$out.next"; return 0
  fi

  if [ ! -f "$file" ]; then
    echo "    no recorded sequence: $file"; rm -f "$out"; return 1
  fi
  if ! same_file "$file" "$out"; then
    echo "    expected:"; sed 's/^/      /' "$file"
    echo "    actual:";   sed 's/^/      /' "$out"
    rm -f "$out"; return 1
  fi
  rm -f "$out"
}

# --- gadget cases ------------------------------------------------------------

run_locale_live() {
  orig=$(vconftool get db/menu_widget/language | sed 's/.*value = \([^ ]*\) .*/\1/')
  case $orig in ko*) other=en_US.UTF-8;; *) other=ko_KR.UTF-8;; esac
  app_launcher -t $GADGET_HOST >/dev/null 2>&1; sleep 1; dlogutil -c
  app_launcher -s $GADGET_HOST __CASE locale_live >/dev/null 2>&1; sleep 2
  vconftool set -f -t string db/menu_widget/language $other >/dev/null
  sleep 5
  vconftool set -f -t string db/menu_widget/language $orig >/dev/null
  dlogutil -d GADGET_TEST DALI_GADGET
}

run_gadget_case() {
  local c=$1 log=/tmp/dali-integration-test.$$ n=0
  if [ "$c" = locale_live ]; then
    run_locale_live > "$log" 2>&1
  else
    # Wait for the result line instead of a fixed time: a slow start (cold
    # cache, load) made a fixed sleep report a passing case as failed.
    app_launcher -t $GADGET_HOST >/dev/null 2>&1; sleep 1; dlogutil -c
    app_launcher -s $GADGET_HOST __CASE $c >/dev/null 2>&1
    while [ $n -lt 20 ]; do
      dlogutil -d GADGET_TEST | grep -q "RESULT $c " && break
      sleep 1; n=$((n + 1))
    done
    sleep 1
    dlogutil -d GADGET_TEST DALI_GADGET > "$log" 2>&1
  fi
  local rc=0
  if ! grep -q "RESULT $c PASS" "$log" || ! check_gadget_side "$log" "$c" >> "$log.out"; then
    rc=1
    grep -E "CHECK FAILED|RESULT|DALI_GADGET" "$log" | sed 's/^/    /'
    [ -f "$log.out" ] && sed 's/^/    /' "$log.out"
  fi
  rm -f "$log" "$log.out"
  return $rc
}

# --- commands ----------------------------------------------------------------

# unified-backend exits 0 and prints "Finish" even when it rejects a package,
# so its exit status says nothing. A package counts as registered when the
# installer was created and pkgcmd lists the package afterwards. Only the
# reference application is optional (it is built on request); any other
# package without a manifest means the package is not installed.
cmd_setup() {
  local pkg out rc=0
  for pkg in $PACKAGES; do
    if [ ! -f /usr/share/packages/$pkg.xml ]; then
      [ "$pkg" = "$REFERENCE" ] && continue
      echo "    $pkg manifest is missing"
      rc=1
      continue
    fi
    out=$(unified-backend --preload -y $pkg 2>&1)
    if echo "$out" | grep -q "Failed to create installer" ||
       ! pkgcmd -l 2>/dev/null | grep -q "pkgid \[$pkg\]"; then
      echo "    $pkg was not registered:"
      echo "$out" | grep -E "Fail|rror" | sed 's/^/      /'
      rc=1
    else
      echo "    $pkg registered"
    fi
  done
  return $rc
}

cmd_list() { echo $ALL_CASES | tr ' ' '\n'; }

# $1: "run" or "record", rest: cases.
cmd_run() {
  local mode=$1 c rc pass=0 fail=0 skip=0 cases; shift
  cases=${*:-$ALL_CASES}
  [ "$mode" = record ] && cases=${*:-$LIFECYCLE_CASES}   # the reference runs these
  for c in $cases; do
    case $c in
      lc_*)
        if [ "$mode" = record ] && echo " $DALI_ONLY_CASES " | grep -q " $c "; then
          echo "    $c has no reference run to record"; skip=$((skip + 1))
          echo "SKIP $c"; continue
        fi
        run_lifecycle_case $c $mode ;;
      *) run_gadget_case $c ;;
    esac
    rc=$?
    case $rc in
      0) echo "PASS $c"; pass=$((pass + 1)) ;;
      2) echo "SKIP $c"; skip=$((skip + 1)) ;;
      *) echo "FAIL $c"; fail=$((fail + 1)) ;;
    esac
  done
  echo "passed=$pass failed=$fail skipped=$skip"
  [ $fail -eq 0 ]
}

if [ "$(id -u)" != 0 ]; then
  echo "run as root (sdb root on)"; exit 1
fi

cmd=${1:-run}
case $cmd in
  setup) cmd_setup ;;
  list) cmd_list ;;
  run) shift; cmd_run run "$@" ;;
  record) shift; cmd_run record "$@" ;;
  *) cmd_run run "$@" ;;
esac
