--- /dev/null
+# Copyright (C) 2013-2014 Miroslav Lichvar <mlichvar@redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+export LC_ALL=C
+export PATH=../../:$PATH
+export CLKNETSIM_PATH=clknetsim
+
+# Known working clknetsim revision
+clknetsim_revision=eb0b85335c40027ac941b2276142542fff482107
+clknetsim_url=https://github.com/mlichvar/clknetsim/archive/$clknetsim_revision.tar.gz
+
+# Only Linux is supported
+if [ "$(uname -s)" != Linux ]; then
+ echo "Simulation tests supported only on Linux"
+ exit 3
+fi
+
+# Try to download clknetsim if not found
+if [ ! -e $CLKNETSIM_PATH ]; then
+ curl -L "$clknetsim_url" | tar xz || exit 3
+ ln -s clknetsim-$clknetsim_revision clknetsim || exit 3
+fi
+
+# Try to build clknetsim if not built
+if [ ! -x $CLKNETSIM_PATH/clknetsim -o ! -e $CLKNETSIM_PATH/clknetsim.so ]; then
+ make -C clknetsim || exit 3
+fi
+
+. $CLKNETSIM_PATH/clknetsim.bash
+
+# Default test testings
+
+default_limit=10000
+default_time_offset=1e-1
+default_freq_offset=1e-4
+default_base_delay=1e-4
+default_jitter=1e-4
+default_wander=1e-9
+default_refclock_jitter=""
+
+default_update_interval=0
+default_log_packets=0
+default_shift_pll=2
+
+default_server_strata=1
+default_servers=1
+default_clients=1
+default_peers=0
+default_server_start=0.0
+default_client_start=0.0
+default_chronyc_start=1000.0
+default_server_step=""
+default_client_step=""
+
+default_server_server_options=""
+default_client_server_options=""
+default_server_peer_options=""
+default_client_peer_options=""
+default_server_conf=""
+default_client_conf=""
+default_chronyc_conf=""
+default_chronyd_options=""
+
+default_time_max_limit=1e-3
+default_freq_max_limit=5e-4
+default_time_rms_limit=2e-4
+default_freq_rms_limit=1e-5
+default_min_sync_time=120
+default_max_sync_time=210
+
+# Initialize test settings from their defaults
+for defopt in $(declare | grep '^default_'); do
+ defoptname=${defopt%%=*}
+ optname=${defoptname#default_}
+ eval "[ -z \"\${$optname:+a}\" ] && $optname=\"\$$defoptname\""
+done
+
+test_start() {
+ rm -f tmp/*
+ echo "Testing $@:"
+}
+
+test_pass() {
+ echo "PASS"
+ exit 0
+}
+
+test_fail() {
+ echo "FAIL"
+ exit 1
+}
+
+test_ok() {
+ pad_line
+ echo -e "\tOK"
+ return 0
+}
+
+test_bad() {
+ pad_line
+ echo -e "\tBAD"
+ return 1
+}
+
+test_error() {
+ pad_line
+ echo -e "\tERROR"
+ return 1
+}
+
+msg_length=0
+pad_line() {
+ local line_length=56
+ [ $msg_length -lt $line_length ] && \
+ printf "%$[$line_length - $msg_length]s" ""
+ msg_length=0
+}
+
+# Print aligned message
+test_message() {
+ local level=$1 eol=$2
+ shift 2
+ local msg="$*"
+
+ while [ $level -gt 0 ]; do
+ echo -n " "
+ level=$[$level - 1]
+ msg_length=$[$msg_length + 2]
+ done
+ echo -n "$msg"
+
+ msg_length=$[$msg_length + ${#msg}]
+ if [ $eol -ne 0 ]; then
+ echo
+ msg_length=0
+ fi
+}
+
+get_wander_expr() {
+ local scaled_wander
+
+ scaled_wander=$(awk "BEGIN {print $wander / \
+ sqrt($update_interval < 0 ? 2^-($update_interval) : 1)}")
+
+ echo "(+ $freq_offset (sum (* $scaled_wander (normal))))"
+}
+
+
+get_delay_expr() {
+ echo "(+ $base_delay (* $jitter (exponential)))"
+}
+
+get_refclock_expr() {
+ echo "(* $refclock_jitter (normal))"
+}
+
+get_chronyd_nodes() {
+ echo $[$servers * $server_strata + $clients]
+}
+
+get_chronyd_conf() {
+ local i stratum=$1 peer=$2
+
+ if [ $stratum -eq 1 ]; then
+ echo "local stratum 1"
+ echo "$server_conf"
+ elif [ $stratum -le $server_strata ]; then
+ for i in $(seq 1 $servers); do
+ echo "server 192.168.123.$[$servers * ($stratum - 2) + $i] $server_server_options"
+ done
+ for i in $(seq 1 $peers); do
+ [ $i -eq $peer -o $i -gt $servers ] && continue
+ echo "peer 192.168.123.$[$servers * ($stratum - 1) + $i] $server_peer_options"
+ done
+ echo "$server_conf"
+ else
+ for i in $(seq 1 $servers); do
+ echo "server 192.168.123.$[$servers * ($stratum - 2) + $i] $client_server_options"
+ done
+ for i in $(seq 1 $peers); do
+ [ $i -eq $peer -o $i -gt $clients ] && continue
+ echo "peer 192.168.123.$[$servers * ($stratum - 1) + $i] $client_peer_options"
+ done
+ echo "$client_conf"
+ fi
+}
+
+# Check if the clock was well synchronized
+check_sync() {
+ local i sync_time max_time_error max_freq_error ret=0
+ local rms_time_error rms_freq_error
+
+ test_message 2 1 "checking clock sync time, max/rms time/freq error:"
+
+ for i in $(seq 1 $(get_chronyd_nodes)); do
+ [ $i -gt $[$servers * $server_strata] ] || continue
+
+ sync_time=$(find_sync tmp/log.offset tmp/log.freq $i \
+ $time_max_limit $freq_max_limit 1.0)
+ max_time_error=$(get_stat 'Maximum absolute offset' $i)
+ max_freq_error=$(get_stat 'Maximum absolute frequency' $i)
+ rms_time_error=$(get_stat 'RMS offset' $i)
+ rms_freq_error=$(get_stat 'RMS frequency' $i)
+
+ test_message 3 0 "node $i: $sync_time $(printf '%.2e %.2e %.2e %.2e' \
+ $max_time_error $max_freq_error $rms_time_error $rms_freq_error)"
+
+ check_stat $sync_time $min_sync_time $max_sync_time && \
+ check_stat $max_time_error 0.0 $time_max_limit && \
+ check_stat $max_freq_error 0.0 $freq_max_limit && \
+ check_stat $rms_time_error 0.0 $time_rms_limit && \
+ check_stat $rms_freq_error 0.0 $freq_rms_limit && \
+ test_ok || test_bad
+
+ [ $? -eq 0 ] || ret=1
+ done
+
+ return $ret
+}
+
+# Check if chronyd exited properly
+check_chronyd_exit() {
+ local i ret=0
+
+ test_message 2 1 "checking chronyd exit:"
+
+ for i in $(seq 1 $(get_chronyd_nodes)); do
+ test_message 3 0 "node $i:"
+
+ tail -n 1 tmp/log.$i | grep -q 'chronyd exiting' && \
+ test_ok || test_bad
+ [ $? -eq 0 ] || ret=1
+ done
+
+ return $ret
+}
+
+# Check for problems in source selection
+check_source_selection() {
+ local i ret=0
+
+ test_message 2 1 "checking source selection:"
+
+ for i in $(seq $[$servers * $server_strata + 1] $(get_chronyd_nodes)); do
+ test_message 3 0 "node $i:"
+
+ ! grep -q 'no majority\|no reachable sources' tmp/log.$i && \
+ grep -q 'Selected source' tmp/log.$i && \
+ test_ok || test_bad
+ [ $? -eq 0 ] || ret=1
+ done
+
+ return $ret
+}
+
+# Check if incoming and outgoing packet intervals are sane
+check_packet_interval() {
+ local i ret=0 in_interval out_interval
+
+ test_message 2 1 "checking incoming and outgoing packet interval:"
+
+ for i in $(seq 1 $(get_chronyd_nodes)); do
+ in_interval=$(get_stat 'Mean incoming packet interval' $i)
+ out_interval=$(get_stat 'Mean outgoing packet interval' $i)
+
+ test_message 3 0 "node $i: $(printf '%.2e %.2e' \
+ $in_interval $out_interval)"
+
+ # Check that the intervals are non-zero and shorter than limit,
+ # incoming is not longer than outgoing for stratum 1 servers, and
+ # outgoing is not longer than incoming for clients.
+ nodes=$[$servers * $server_strata + $clients]
+ check_stat $in_interval 0.1 $limit && \
+ check_stat $out_interval 0.1 $limit && \
+ ([ $i -gt $servers ] || \
+ check_stat $in_interval 0.0 $out_interval) && \
+ ([ $i -le $[$servers * $server_strata] ] || \
+ check_stat $in_interval 0.0 $out_interval) && \
+ test_ok || test_bad
+
+ [ $? -eq 0 ] || ret=1
+ done
+
+ return $ret
+}
+
+# Compare chronyc output with specified pattern
+check_chronyc_output() {
+ local i ret=0 pattern=$1
+
+ test_message 2 1 "checking chronyc output:"
+
+ for i in $(seq $[$(get_chronyd_nodes) + 1] $[$(get_chronyd_nodes) + $clients]); do
+ test_message 3 0 "node $i:"
+
+ [[ "$(cat tmp/log.$i)" =~ $pattern ]] && \
+ test_ok || test_bad
+ [ $? -eq 0 ] || ret=1
+ done
+
+ return $ret
+}
+
+# Print test settings which differ from default value
+print_nondefaults() {
+ local defopt defoptname optname
+
+ test_message 2 1 "non-default settings:"
+ declare | grep '^default_*' | while read defopt; do
+ defoptname=${defopt%%=*}
+ optname=${defoptname#default_}
+ eval "[ \"\$$optname\" = \"\$$defoptname\" ]" || \
+ test_message 3 1 $(eval "echo $optname=\$$optname")
+ done
+}
+
+run_simulation() {
+ local nodes=$1
+
+ test_message 2 0 "running simulation:"
+
+ start_server $nodes \
+ -o tmp/log.offset -f tmp/log.freq \
+ $([ $log_packets -ne 0 ] && echo -p tmp/log.packets) \
+ -R $(awk "BEGIN {print $update_interval < 0 ? 2^-($update_interval) : 1}") \
+ -r $(awk "BEGIN {print $max_sync_time * 2^$update_interval}") \
+ -l $(awk "BEGIN {print $limit * 2^$update_interval}") && test_ok || test_error
+}
+
+run_test() {
+ local i j n stratum node nodes step start freq offset conf
+
+ test_message 1 1 "network with $servers*$server_strata servers and $clients clients:"
+ print_nondefaults
+
+ nodes=$(get_chronyd_nodes)
+ [ -n "$chronyc_conf" ] && nodes=$[$nodes + $clients]
+
+ for i in $(seq 1 $nodes); do
+ echo "node${i}_shift_pll = $shift_pll"
+ for j in $(seq 1 $nodes); do
+ [ $i -eq $j ] && continue
+ echo "node${i}_delay${j} = $(get_delay_expr)"
+ echo "node${j}_delay${i} = $(get_delay_expr)"
+ done
+ done > tmp/conf
+
+ node=1
+
+ for stratum in $(seq 1 $[$server_strata + 1]); do
+ [ $stratum -le $server_strata ] && n=$servers || n=$clients
+
+ for i in $(seq 1 $n); do
+ test_message 2 0 "starting node $node:"
+ if [ $stratum -eq 1 ]; then
+ step=$server_step
+ start=$server_start
+ freq=""
+ offset=0.0
+ elif [ $stratum -le $server_strata ]; then
+ step=$server_step
+ start=$server_start
+ freq=$(get_wander_expr)
+ offset=0.0
+ else
+ step=$client_step
+ start=$client_start
+ freq=$(get_wander_expr)
+ offset=$time_offset
+ fi
+
+ conf=$(get_chronyd_conf $stratum $i $n)
+
+ [ -z "$freq" ] || echo "node${node}_freq = $freq" >> tmp/conf
+ [ -z "$step" ] || echo "node${node}_step = $step" >> tmp/conf
+ [ -z "$refclock_jitter" ] || \
+ echo "node${node}_refclock = $(get_refclock_expr)" >> tmp/conf
+ echo "node${node}_offset = $offset" >> tmp/conf
+ echo "node${node}_start = $start" >> tmp/conf
+ start_client $node chronyd "$conf" "" "$chronyd_options" && \
+ test_ok || test_error
+
+ [ $? -ne 0 ] && return 1
+ node=$[$node + 1]
+ done
+ done
+
+ for i in $(seq 1 $[$nodes - $node + 1]); do
+ test_message 2 0 "starting node $node:"
+
+ echo "node${node}_start = $chronyc_start" >> tmp/conf
+ start_client $node chronyc "$chronyc_conf" "" \
+ "-n -h 192.168.123.$[$node - $clients]" && \
+ test_ok || test_error
+
+ [ $? -ne 0 ] && return 1
+ node=$[$node + 1]
+ done
+
+ run_simulation $nodes
+}