1*2abb3134SXin Li#!/bin/bash 2*2abb3134SXin Liusage() { 3*2abb3134SXin Liecho " 4*2abb3134SXin Li Run end-to-end tests in parallel. 5*2abb3134SXin Li 6*2abb3134SXin Li Usage: 7*2abb3134SXin Li ./regtest.sh <function name> 8*2abb3134SXin Li At the end, it will print an HTML summary. 9*2abb3134SXin Li 10*2abb3134SXin Li Three main functions are 11*2abb3134SXin Li run [<pattern> [<lang>]] - run tests matching <pattern> in 12*2abb3134SXin Li parallel. The language 13*2abb3134SXin Li of the client to use. 14*2abb3134SXin Li run-seq [<pattern> [<lang>]] - ditto, except that tests are run 15*2abb3134SXin Li sequentially 16*2abb3134SXin Li run-all - run all tests, in parallel 17*2abb3134SXin Li 18*2abb3134SXin Li Examples: 19*2abb3134SXin Li $ ./regtest.sh run-seq unif-small-typical # Run, the unif-small-typical test 20*2abb3134SXin Li $ ./regtest.sh run-seq unif-small- # Sequential, the tests containing: 21*2abb3134SXin Li # 'unif-small-' 22*2abb3134SXin Li $ ./regtest.sh run unif- # Parallel run, matches multiple cases 23*2abb3134SXin Li $ ./regtest.sh run-all # Run all tests 24*2abb3134SXin Li 25*2abb3134SXin Li The <pattern> argument is a regex in 'grep -E' format. (Detail: Don't 26*2abb3134SXin Li use $ in the pattern, since it matches the whole spec line and not just the 27*2abb3134SXin Li test case name.) The number of processors used in a parallel run is one less 28*2abb3134SXin Li than the number of CPUs on the machine. 29*2abb3134SXin Li" 30*2abb3134SXin Li} 31*2abb3134SXin Li# Future speedups: 32*2abb3134SXin Li# - Reuse the same input -- come up with naming scheme based on params 33*2abb3134SXin Li# - Reuse the same maps -- ditto, rappor library can cache it 34*2abb3134SXin Li# 35*2abb3134SXin Li 36*2abb3134SXin Liset -o nounset 37*2abb3134SXin Liset -o pipefail 38*2abb3134SXin Liset -o errexit 39*2abb3134SXin Li 40*2abb3134SXin Li. util.sh 41*2abb3134SXin Li 42*2abb3134SXin Lireadonly THIS_DIR=$(dirname $0) 43*2abb3134SXin Lireadonly REPO_ROOT=$THIS_DIR 44*2abb3134SXin Lireadonly CLIENT_DIR=$REPO_ROOT/client/python 45*2abb3134SXin Li# subdirs are in _tmp/$impl, which shouldn't overlap with anything else in _tmp 46*2abb3134SXin Lireadonly REGTEST_BASE_DIR=_tmp 47*2abb3134SXin Li 48*2abb3134SXin Li# All the Python tools need this 49*2abb3134SXin Liexport PYTHONPATH=$CLIENT_DIR 50*2abb3134SXin Li 51*2abb3134SXin Liprint-unique-values() { 52*2abb3134SXin Li local num_unique_values=$1 53*2abb3134SXin Li seq 1 $num_unique_values | awk '{print "v" $1}' 54*2abb3134SXin Li} 55*2abb3134SXin Li 56*2abb3134SXin Li# Add some more candidates here. We hope these are estimated at 0. 57*2abb3134SXin Li# e.g. if add_start=51, and num_additional is 20, show v51-v70 58*2abb3134SXin Limore-candidates() { 59*2abb3134SXin Li local last_true=$1 60*2abb3134SXin Li local num_additional=$2 61*2abb3134SXin Li 62*2abb3134SXin Li local begin 63*2abb3134SXin Li local end 64*2abb3134SXin Li begin=$(expr $last_true + 1) 65*2abb3134SXin Li end=$(expr $last_true + $num_additional) 66*2abb3134SXin Li 67*2abb3134SXin Li seq $begin $end | awk '{print "v" $1}' 68*2abb3134SXin Li} 69*2abb3134SXin Li 70*2abb3134SXin Li# Args: 71*2abb3134SXin Li# unique_values: File of unique true values 72*2abb3134SXin Li# last_true: last true input, e.g. 50 if we generated "v1" .. "v50". 73*2abb3134SXin Li# num_additional: additional candidates to generate (starting at 'last_true') 74*2abb3134SXin Li# to_remove: Regex of true values to omit from the candidates list, or the 75*2abb3134SXin Li# string 'NONE' if none should be. (Our values look like 'v1', 'v2', etc. so 76*2abb3134SXin Li# there isn't any ambiguity.) 77*2abb3134SXin Liprint-candidates() { 78*2abb3134SXin Li local unique_values=$1 79*2abb3134SXin Li local last_true=$2 80*2abb3134SXin Li local num_additional=$3 81*2abb3134SXin Li local to_remove=$4 82*2abb3134SXin Li 83*2abb3134SXin Li if test $to_remove = NONE; then 84*2abb3134SXin Li cat $unique_values # include all true inputs 85*2abb3134SXin Li else 86*2abb3134SXin Li egrep -v $to_remove $unique_values # remove some true inputs 87*2abb3134SXin Li fi 88*2abb3134SXin Li more-candidates $last_true $num_additional 89*2abb3134SXin Li} 90*2abb3134SXin Li 91*2abb3134SXin Li# Generate a single test case, specified by a line of the test spec. 92*2abb3134SXin Li# This is a helper function for _run_tests(). 93*2abb3134SXin Li_setup-one-case() { 94*2abb3134SXin Li local impl=$1 95*2abb3134SXin Li shift # impl is not part of the spec; the next 13 params are 96*2abb3134SXin Li 97*2abb3134SXin Li local test_case=$1 98*2abb3134SXin Li 99*2abb3134SXin Li # input params 100*2abb3134SXin Li local dist=$2 101*2abb3134SXin Li local num_unique_values=$3 102*2abb3134SXin Li local num_clients=$4 103*2abb3134SXin Li local values_per_client=$5 104*2abb3134SXin Li 105*2abb3134SXin Li # RAPPOR params 106*2abb3134SXin Li local num_bits=$6 107*2abb3134SXin Li local num_hashes=$7 108*2abb3134SXin Li local num_cohorts=$8 109*2abb3134SXin Li local p=$9 110*2abb3134SXin Li local q=${10} # need curly braces to get the 10th arg 111*2abb3134SXin Li local f=${11} 112*2abb3134SXin Li 113*2abb3134SXin Li # map params 114*2abb3134SXin Li local num_additional=${12} 115*2abb3134SXin Li local to_remove=${13} 116*2abb3134SXin Li 117*2abb3134SXin Li banner 'Setting up parameters and candidate files for '$test_case 118*2abb3134SXin Li 119*2abb3134SXin Li local case_dir=$REGTEST_BASE_DIR/$impl/$test_case 120*2abb3134SXin Li mkdir --verbose -p $case_dir 121*2abb3134SXin Li 122*2abb3134SXin Li # Save the "spec" 123*2abb3134SXin Li echo "$@" > $case_dir/spec.txt 124*2abb3134SXin Li 125*2abb3134SXin Li local params_path=$case_dir/case_params.csv 126*2abb3134SXin Li 127*2abb3134SXin Li echo 'k,h,m,p,q,f' > $params_path 128*2abb3134SXin Li echo "$num_bits,$num_hashes,$num_cohorts,$p,$q,$f" >> $params_path 129*2abb3134SXin Li 130*2abb3134SXin Li print-unique-values $num_unique_values > $case_dir/case_unique_values.txt 131*2abb3134SXin Li 132*2abb3134SXin Li local true_map_path=$case_dir/case_true_map.csv 133*2abb3134SXin Li 134*2abb3134SXin Li bin/hash_candidates.py \ 135*2abb3134SXin Li $params_path \ 136*2abb3134SXin Li < $case_dir/case_unique_values.txt \ 137*2abb3134SXin Li > $true_map_path 138*2abb3134SXin Li 139*2abb3134SXin Li # banner "Constructing candidates" 140*2abb3134SXin Li 141*2abb3134SXin Li print-candidates \ 142*2abb3134SXin Li $case_dir/case_unique_values.txt $num_unique_values \ 143*2abb3134SXin Li $num_additional "$to_remove" \ 144*2abb3134SXin Li > $case_dir/case_candidates.txt 145*2abb3134SXin Li 146*2abb3134SXin Li # banner "Hashing candidates to get 'map'" 147*2abb3134SXin Li 148*2abb3134SXin Li bin/hash_candidates.py \ 149*2abb3134SXin Li $params_path \ 150*2abb3134SXin Li < $case_dir/case_candidates.txt \ 151*2abb3134SXin Li > $case_dir/case_map.csv 152*2abb3134SXin Li} 153*2abb3134SXin Li 154*2abb3134SXin Li# Run a single test instance, specified by <test_name, instance_num>. 155*2abb3134SXin Li# This is a helper function for _run_tests(). 156*2abb3134SXin Li_run-one-instance() { 157*2abb3134SXin Li local test_case=$1 158*2abb3134SXin Li local test_instance=$2 159*2abb3134SXin Li local impl=$3 160*2abb3134SXin Li 161*2abb3134SXin Li local case_dir=$REGTEST_BASE_DIR/$impl/$test_case 162*2abb3134SXin Li 163*2abb3134SXin Li read -r \ 164*2abb3134SXin Li case_name distr num_unique_values num_clients values_per_client \ 165*2abb3134SXin Li num_bits num_hashes num_cohorts p q f \ 166*2abb3134SXin Li num_additional to_remove \ 167*2abb3134SXin Li < $case_dir/spec.txt 168*2abb3134SXin Li 169*2abb3134SXin Li local instance_dir=$case_dir/$test_instance 170*2abb3134SXin Li mkdir --verbose -p $instance_dir 171*2abb3134SXin Li 172*2abb3134SXin Li banner "Generating reports (gen_reports.R)" 173*2abb3134SXin Li 174*2abb3134SXin Li # the TRUE_VALUES_PATH environment variable can be used to avoid 175*2abb3134SXin Li # generating new values every time. NOTE: You are responsible for making 176*2abb3134SXin Li # sure the params match! 177*2abb3134SXin Li 178*2abb3134SXin Li local true_values=${TRUE_VALUES_PATH:-} 179*2abb3134SXin Li if test -z "$true_values"; then 180*2abb3134SXin Li true_values=$instance_dir/case_true_values.csv 181*2abb3134SXin Li tests/gen_true_values.R $distr $num_unique_values $num_clients \ 182*2abb3134SXin Li $values_per_client $num_cohorts \ 183*2abb3134SXin Li $true_values 184*2abb3134SXin Li else 185*2abb3134SXin Li # TEMP hack: Make it visible to plot. 186*2abb3134SXin Li # TODO: Fix compare_dist.R 187*2abb3134SXin Li ln -s -f --verbose \ 188*2abb3134SXin Li $PWD/$true_values \ 189*2abb3134SXin Li $instance_dir/case_true_values.csv 190*2abb3134SXin Li fi 191*2abb3134SXin Li 192*2abb3134SXin Li case $impl in 193*2abb3134SXin Li python) 194*2abb3134SXin Li banner "Running RAPPOR Python client" 195*2abb3134SXin Li 196*2abb3134SXin Li # Writes encoded "out" file, true histogram, true inputs to 197*2abb3134SXin Li # $instance_dir. 198*2abb3134SXin Li time tests/rappor_sim.py \ 199*2abb3134SXin Li --num-bits $num_bits \ 200*2abb3134SXin Li --num-hashes $num_hashes \ 201*2abb3134SXin Li --num-cohorts $num_cohorts \ 202*2abb3134SXin Li -p $p \ 203*2abb3134SXin Li -q $q \ 204*2abb3134SXin Li -f $f \ 205*2abb3134SXin Li < $true_values \ 206*2abb3134SXin Li > "$instance_dir/case_reports.csv" 207*2abb3134SXin Li ;; 208*2abb3134SXin Li 209*2abb3134SXin Li cpp) 210*2abb3134SXin Li banner "Running RAPPOR C++ client (see rappor_sim.log for errors)" 211*2abb3134SXin Li 212*2abb3134SXin Li time client/cpp/_tmp/rappor_sim \ 213*2abb3134SXin Li $num_bits \ 214*2abb3134SXin Li $num_hashes \ 215*2abb3134SXin Li $num_cohorts \ 216*2abb3134SXin Li $p \ 217*2abb3134SXin Li $q \ 218*2abb3134SXin Li $f \ 219*2abb3134SXin Li < $true_values \ 220*2abb3134SXin Li > "$instance_dir/case_reports.csv" \ 221*2abb3134SXin Li 2>"$instance_dir/rappor_sim.log" 222*2abb3134SXin Li ;; 223*2abb3134SXin Li 224*2abb3134SXin Li *) 225*2abb3134SXin Li log "Invalid impl $impl (should be one of python|cpp)" 226*2abb3134SXin Li exit 1 227*2abb3134SXin Li ;; 228*2abb3134SXin Li 229*2abb3134SXin Li esac 230*2abb3134SXin Li 231*2abb3134SXin Li banner "Summing RAPPOR IRR bits to get 'counts'" 232*2abb3134SXin Li 233*2abb3134SXin Li bin/sum_bits.py \ 234*2abb3134SXin Li $case_dir/case_params.csv \ 235*2abb3134SXin Li < $instance_dir/case_reports.csv \ 236*2abb3134SXin Li > $instance_dir/case_counts.csv 237*2abb3134SXin Li 238*2abb3134SXin Li local out_dir=${instance_dir}_report 239*2abb3134SXin Li mkdir --verbose -p $out_dir 240*2abb3134SXin Li 241*2abb3134SXin Li # Currently, the summary file shows and aggregates timing of the inference 242*2abb3134SXin Li # engine, which excludes R's loading time and reading of the (possibly 243*2abb3134SXin Li # substantial) map file. Timing below is more inclusive. 244*2abb3134SXin Li TIMEFORMAT='Running compare_dist.R took %R seconds' 245*2abb3134SXin Li time { 246*2abb3134SXin Li # Input prefix, output dir 247*2abb3134SXin Li tests/compare_dist.R -t "Test case: $test_case (instance $test_instance)" \ 248*2abb3134SXin Li "$case_dir/case" "$instance_dir/case" $out_dir 249*2abb3134SXin Li } 250*2abb3134SXin Li} 251*2abb3134SXin Li 252*2abb3134SXin Li# Like _run-once-case, but log to a file. 253*2abb3134SXin Li_run-one-instance-logged() { 254*2abb3134SXin Li local test_case=$1 255*2abb3134SXin Li local test_instance=$2 256*2abb3134SXin Li local impl=$3 257*2abb3134SXin Li 258*2abb3134SXin Li local log_dir=$REGTEST_BASE_DIR/$impl/$test_case/${test_instance}_report 259*2abb3134SXin Li mkdir --verbose -p $log_dir 260*2abb3134SXin Li 261*2abb3134SXin Li log "Started '$test_case' (instance $test_instance) -- logging to $log_dir/log.txt" 262*2abb3134SXin Li _run-one-instance "$@" >$log_dir/log.txt 2>&1 \ 263*2abb3134SXin Li && log "Test case $test_case (instance $test_instance) done" \ 264*2abb3134SXin Li || log "Test case $test_case (instance $test_instance) failed" 265*2abb3134SXin Li} 266*2abb3134SXin Li 267*2abb3134SXin Limake-summary() { 268*2abb3134SXin Li local dir=$1 269*2abb3134SXin Li local impl=$2 270*2abb3134SXin Li 271*2abb3134SXin Li local filename=results.html 272*2abb3134SXin Li 273*2abb3134SXin Li tests/make_summary.py $dir $dir/rows.html 274*2abb3134SXin Li 275*2abb3134SXin Li pushd $dir >/dev/null 276*2abb3134SXin Li 277*2abb3134SXin Li cat ../../tests/regtest.html \ 278*2abb3134SXin Li | sed -e '/__TABLE_ROWS__/ r rows.html' -e "s/_IMPL_/$impl/g" \ 279*2abb3134SXin Li > $filename 280*2abb3134SXin Li 281*2abb3134SXin Li popd >/dev/null 282*2abb3134SXin Li 283*2abb3134SXin Li log "Wrote $dir/$filename" 284*2abb3134SXin Li log "URL: file://$PWD/$dir/$filename" 285*2abb3134SXin Li} 286*2abb3134SXin Li 287*2abb3134SXin Litest-error() { 288*2abb3134SXin Li local spec_regex=${1:-} 289*2abb3134SXin Li log "Some test cases failed" 290*2abb3134SXin Li if test -n "$spec_regex"; then 291*2abb3134SXin Li log "(Perhaps none matched pattern '$spec_regex')" 292*2abb3134SXin Li fi 293*2abb3134SXin Li # don't quit just yet 294*2abb3134SXin Li # exit 1 295*2abb3134SXin Li} 296*2abb3134SXin Li 297*2abb3134SXin Li# Assuming the spec file, write a list of test case names (first column) with 298*2abb3134SXin Li# the instance ids (second column), where instance ids run from 1 to $1. 299*2abb3134SXin Li# Third column is impl. 300*2abb3134SXin Li_setup-test-instances() { 301*2abb3134SXin Li local instances=$1 302*2abb3134SXin Li local impl=$2 303*2abb3134SXin Li 304*2abb3134SXin Li while read line; do 305*2abb3134SXin Li for i in $(seq 1 $instances); do 306*2abb3134SXin Li read case_name _ <<< $line # extract the first token 307*2abb3134SXin Li echo $case_name $i $impl 308*2abb3134SXin Li done 309*2abb3134SXin Li done 310*2abb3134SXin Li} 311*2abb3134SXin Li 312*2abb3134SXin Li# Print the default number of parallel processes, which is max(#CPUs - 1, 1) 313*2abb3134SXin Lidefault-processes() { 314*2abb3134SXin Li processors=$(grep -c ^processor /proc/cpuinfo || echo 4) # Linux-specific 315*2abb3134SXin Li if test $processors -gt 1; then # leave one CPU for the OS 316*2abb3134SXin Li processors=$(expr $processors - 1) 317*2abb3134SXin Li fi 318*2abb3134SXin Li echo $processors 319*2abb3134SXin Li} 320*2abb3134SXin Li 321*2abb3134SXin Li# Args: 322*2abb3134SXin Li# spec_gen: A program to execute to generate the spec. 323*2abb3134SXin Li# spec_regex: A pattern selecting the subset of tests to run 324*2abb3134SXin Li# parallel: Whether the tests are run in parallel (T/F). Sequential 325*2abb3134SXin Li# runs log to the console; parallel runs log to files. 326*2abb3134SXin Li# impl: one of python, or cpp 327*2abb3134SXin Li# instances: A number of times each test case is run 328*2abb3134SXin Li 329*2abb3134SXin Li_run-tests() { 330*2abb3134SXin Li local spec_gen=$1 331*2abb3134SXin Li local spec_regex="$2" # grep -E format on the spec, can be empty 332*2abb3134SXin Li local parallel=$3 333*2abb3134SXin Li local impl=${4:-"cpp"} 334*2abb3134SXin Li local instances=${5:-1} 335*2abb3134SXin Li 336*2abb3134SXin Li local regtest_dir=$REGTEST_BASE_DIR/$impl 337*2abb3134SXin Li rm -r -f --verbose $regtest_dir 338*2abb3134SXin Li 339*2abb3134SXin Li mkdir --verbose -p $regtest_dir 340*2abb3134SXin Li 341*2abb3134SXin Li local func 342*2abb3134SXin Li local processors 343*2abb3134SXin Li 344*2abb3134SXin Li if test $parallel = F; then 345*2abb3134SXin Li func=_run-one-instance # output to the console 346*2abb3134SXin Li processors=1 347*2abb3134SXin Li else 348*2abb3134SXin Li func=_run-one-instance-logged 349*2abb3134SXin Li # Let the user override with MAX_PROC, in case they don't have enough 350*2abb3134SXin Li # memory. 351*2abb3134SXin Li processors=${MAX_PROC:-$(default-processes)} 352*2abb3134SXin Li log "Running $processors parallel processes" 353*2abb3134SXin Li fi 354*2abb3134SXin Li 355*2abb3134SXin Li local cases_list=$regtest_dir/test-cases.txt 356*2abb3134SXin Li # Need -- for regexes that start with - 357*2abb3134SXin Li $spec_gen | grep -E -- "$spec_regex" > $cases_list 358*2abb3134SXin Li 359*2abb3134SXin Li # Generate parameters for all test cases. 360*2abb3134SXin Li cat $cases_list \ 361*2abb3134SXin Li | xargs -l -P $processors -- $0 _setup-one-case $impl \ 362*2abb3134SXin Li || test-error 363*2abb3134SXin Li 364*2abb3134SXin Li log "Done generating parameters for all test cases" 365*2abb3134SXin Li 366*2abb3134SXin Li local instances_list=$regtest_dir/test-instances.txt 367*2abb3134SXin Li _setup-test-instances $instances $impl < $cases_list > $instances_list 368*2abb3134SXin Li 369*2abb3134SXin Li cat $instances_list \ 370*2abb3134SXin Li | xargs -l -P $processors -- $0 $func || test-error 371*2abb3134SXin Li 372*2abb3134SXin Li log "Done running all test instances" 373*2abb3134SXin Li 374*2abb3134SXin Li make-summary $regtest_dir $impl 375*2abb3134SXin Li} 376*2abb3134SXin Li 377*2abb3134SXin Li# used for most tests 378*2abb3134SXin Lireadonly REGTEST_SPEC=tests/regtest_spec.py 379*2abb3134SXin Li 380*2abb3134SXin Li# Run tests sequentially. NOTE: called by demo.sh. 381*2abb3134SXin Lirun-seq() { 382*2abb3134SXin Li local spec_regex=${1:-'^r-'} # grep -E format on the spec 383*2abb3134SXin Li shift 384*2abb3134SXin Li 385*2abb3134SXin Li time _run-tests $REGTEST_SPEC $spec_regex F $@ 386*2abb3134SXin Li} 387*2abb3134SXin Li 388*2abb3134SXin Li# Run tests in parallel 389*2abb3134SXin Lirun() { 390*2abb3134SXin Li local spec_regex=${1:-'^r-'} # grep -E format on the spec 391*2abb3134SXin Li shift 392*2abb3134SXin Li 393*2abb3134SXin Li time _run-tests $REGTEST_SPEC $spec_regex T $@ 394*2abb3134SXin Li} 395*2abb3134SXin Li 396*2abb3134SXin Li# Run tests in parallel (7+ minutes on 8 cores) 397*2abb3134SXin Lirun-all() { 398*2abb3134SXin Li log "Running all tests. Can take a while." 399*2abb3134SXin Li time _run-tests $REGTEST_SPEC '^r-' T cpp 400*2abb3134SXin Li} 401*2abb3134SXin Li 402*2abb3134SXin Lirun-user() { 403*2abb3134SXin Li local spec_regex=${1:-} 404*2abb3134SXin Li local parallel=T # too much memory 405*2abb3134SXin Li time _run-tests tests/user_spec.py "$spec_regex" $parallel cpp 406*2abb3134SXin Li} 407*2abb3134SXin Li 408*2abb3134SXin Li# Use stable true values 409*2abb3134SXin Licompare-python-cpp() { 410*2abb3134SXin Li local num_unique_values=100 411*2abb3134SXin Li local num_clients=10000 412*2abb3134SXin Li local values_per_client=10 413*2abb3134SXin Li local num_cohorts=64 414*2abb3134SXin Li 415*2abb3134SXin Li local true_values=$REGTEST_BASE_DIR/stable_true_values.csv 416*2abb3134SXin Li 417*2abb3134SXin Li tests/gen_true_values.R \ 418*2abb3134SXin Li exp $num_unique_values $num_clients $values_per_client $num_cohorts \ 419*2abb3134SXin Li $true_values 420*2abb3134SXin Li 421*2abb3134SXin Li wc -l $true_values 422*2abb3134SXin Li 423*2abb3134SXin Li # Run Python and C++ simulation on the same input 424*2abb3134SXin Li 425*2abb3134SXin Li ./build.sh cpp-client 426*2abb3134SXin Li 427*2abb3134SXin Li TRUE_VALUES_PATH=$true_values \ 428*2abb3134SXin Li ./regtest.sh run-seq '^demo3' 1 python 429*2abb3134SXin Li 430*2abb3134SXin Li TRUE_VALUES_PATH=$true_values \ 431*2abb3134SXin Li ./regtest.sh run-seq '^demo3' 1 cpp 432*2abb3134SXin Li 433*2abb3134SXin Li head _tmp/{python,cpp}/demo3/1/case_reports.csv 434*2abb3134SXin Li} 435*2abb3134SXin Li 436*2abb3134SXin Liif test $# -eq 0 ; then 437*2abb3134SXin Li usage 438*2abb3134SXin Lielse 439*2abb3134SXin Li "$@" 440*2abb3134SXin Lifi 441