diff options
Diffstat (limited to 'regtest.sh')
-rwxr-xr-x | regtest.sh | 440 |
1 files changed, 440 insertions, 0 deletions
diff --git a/regtest.sh b/regtest.sh new file mode 100755 index 0000000..d21edc2 --- /dev/null +++ b/regtest.sh @@ -0,0 +1,440 @@ +#!/bin/bash +usage() { +echo " + Run end-to-end tests in parallel. + + Usage: + ./regtest.sh <function name> + At the end, it will print an HTML summary. + + Three main functions are + run [<pattern> [<lang>]] - run tests matching <pattern> in + parallel. The language + of the client to use. + run-seq [<pattern> [<lang>]] - ditto, except that tests are run + sequentially + run-all - run all tests, in parallel + + Examples: + $ ./regtest.sh run-seq unif-small-typical # Run, the unif-small-typical test + $ ./regtest.sh run-seq unif-small- # Sequential, the tests containing: + # 'unif-small-' + $ ./regtest.sh run unif- # Parallel run, matches multiple cases + $ ./regtest.sh run-all # Run all tests + + The <pattern> argument is a regex in 'grep -E' format. (Detail: Don't + use $ in the pattern, since it matches the whole spec line and not just the + test case name.) The number of processors used in a parallel run is one less + than the number of CPUs on the machine. +" +} +# Future speedups: +# - Reuse the same input -- come up with naming scheme based on params +# - Reuse the same maps -- ditto, rappor library can cache it +# + +set -o nounset +set -o pipefail +set -o errexit + +. util.sh + +readonly THIS_DIR=$(dirname $0) +readonly REPO_ROOT=$THIS_DIR +readonly CLIENT_DIR=$REPO_ROOT/client/python +# subdirs are in _tmp/$impl, which shouldn't overlap with anything else in _tmp +readonly REGTEST_BASE_DIR=_tmp + +# All the Python tools need this +export PYTHONPATH=$CLIENT_DIR + +print-unique-values() { + local num_unique_values=$1 + seq 1 $num_unique_values | awk '{print "v" $1}' +} + +# Add some more candidates here. We hope these are estimated at 0. +# e.g. if add_start=51, and num_additional is 20, show v51-v70 +more-candidates() { + local last_true=$1 + local num_additional=$2 + + local begin + local end + begin=$(expr $last_true + 1) + end=$(expr $last_true + $num_additional) + + seq $begin $end | awk '{print "v" $1}' +} + +# Args: +# unique_values: File of unique true values +# last_true: last true input, e.g. 50 if we generated "v1" .. "v50". +# num_additional: additional candidates to generate (starting at 'last_true') +# to_remove: Regex of true values to omit from the candidates list, or the +# string 'NONE' if none should be. (Our values look like 'v1', 'v2', etc. so +# there isn't any ambiguity.) +print-candidates() { + local unique_values=$1 + local last_true=$2 + local num_additional=$3 + local to_remove=$4 + + if test $to_remove = NONE; then + cat $unique_values # include all true inputs + else + egrep -v $to_remove $unique_values # remove some true inputs + fi + more-candidates $last_true $num_additional +} + +# Generate a single test case, specified by a line of the test spec. +# This is a helper function for _run_tests(). +_setup-one-case() { + local impl=$1 + shift # impl is not part of the spec; the next 13 params are + + local test_case=$1 + + # input params + local dist=$2 + local num_unique_values=$3 + local num_clients=$4 + local values_per_client=$5 + + # RAPPOR params + local num_bits=$6 + local num_hashes=$7 + local num_cohorts=$8 + local p=$9 + local q=${10} # need curly braces to get the 10th arg + local f=${11} + + # map params + local num_additional=${12} + local to_remove=${13} + + banner 'Setting up parameters and candidate files for '$test_case + + local case_dir=$REGTEST_BASE_DIR/$impl/$test_case + mkdir --verbose -p $case_dir + + # Save the "spec" + echo "$@" > $case_dir/spec.txt + + local params_path=$case_dir/case_params.csv + + echo 'k,h,m,p,q,f' > $params_path + echo "$num_bits,$num_hashes,$num_cohorts,$p,$q,$f" >> $params_path + + print-unique-values $num_unique_values > $case_dir/case_unique_values.txt + + local true_map_path=$case_dir/case_true_map.csv + + bin/hash_candidates.py \ + $params_path \ + < $case_dir/case_unique_values.txt \ + > $true_map_path + + # banner "Constructing candidates" + + print-candidates \ + $case_dir/case_unique_values.txt $num_unique_values \ + $num_additional "$to_remove" \ + > $case_dir/case_candidates.txt + + # banner "Hashing candidates to get 'map'" + + bin/hash_candidates.py \ + $params_path \ + < $case_dir/case_candidates.txt \ + > $case_dir/case_map.csv +} + +# Run a single test instance, specified by <test_name, instance_num>. +# This is a helper function for _run_tests(). +_run-one-instance() { + local test_case=$1 + local test_instance=$2 + local impl=$3 + + local case_dir=$REGTEST_BASE_DIR/$impl/$test_case + + read -r \ + case_name distr num_unique_values num_clients values_per_client \ + num_bits num_hashes num_cohorts p q f \ + num_additional to_remove \ + < $case_dir/spec.txt + + local instance_dir=$case_dir/$test_instance + mkdir --verbose -p $instance_dir + + banner "Generating reports (gen_reports.R)" + + # the TRUE_VALUES_PATH environment variable can be used to avoid + # generating new values every time. NOTE: You are responsible for making + # sure the params match! + + local true_values=${TRUE_VALUES_PATH:-} + if test -z "$true_values"; then + true_values=$instance_dir/case_true_values.csv + tests/gen_true_values.R $distr $num_unique_values $num_clients \ + $values_per_client $num_cohorts \ + $true_values + else + # TEMP hack: Make it visible to plot. + # TODO: Fix compare_dist.R + ln -s -f --verbose \ + $PWD/$true_values \ + $instance_dir/case_true_values.csv + fi + + case $impl in + python) + banner "Running RAPPOR Python client" + + # Writes encoded "out" file, true histogram, true inputs to + # $instance_dir. + time tests/rappor_sim.py \ + --num-bits $num_bits \ + --num-hashes $num_hashes \ + --num-cohorts $num_cohorts \ + -p $p \ + -q $q \ + -f $f \ + < $true_values \ + > "$instance_dir/case_reports.csv" + ;; + + cpp) + banner "Running RAPPOR C++ client (see rappor_sim.log for errors)" + + time client/cpp/_tmp/rappor_sim \ + $num_bits \ + $num_hashes \ + $num_cohorts \ + $p \ + $q \ + $f \ + < $true_values \ + > "$instance_dir/case_reports.csv" \ + 2>"$instance_dir/rappor_sim.log" + ;; + + *) + log "Invalid impl $impl (should be one of python|cpp)" + exit 1 + ;; + + esac + + banner "Summing RAPPOR IRR bits to get 'counts'" + + bin/sum_bits.py \ + $case_dir/case_params.csv \ + < $instance_dir/case_reports.csv \ + > $instance_dir/case_counts.csv + + local out_dir=${instance_dir}_report + mkdir --verbose -p $out_dir + + # Currently, the summary file shows and aggregates timing of the inference + # engine, which excludes R's loading time and reading of the (possibly + # substantial) map file. Timing below is more inclusive. + TIMEFORMAT='Running compare_dist.R took %R seconds' + time { + # Input prefix, output dir + tests/compare_dist.R -t "Test case: $test_case (instance $test_instance)" \ + "$case_dir/case" "$instance_dir/case" $out_dir + } +} + +# Like _run-once-case, but log to a file. +_run-one-instance-logged() { + local test_case=$1 + local test_instance=$2 + local impl=$3 + + local log_dir=$REGTEST_BASE_DIR/$impl/$test_case/${test_instance}_report + mkdir --verbose -p $log_dir + + log "Started '$test_case' (instance $test_instance) -- logging to $log_dir/log.txt" + _run-one-instance "$@" >$log_dir/log.txt 2>&1 \ + && log "Test case $test_case (instance $test_instance) done" \ + || log "Test case $test_case (instance $test_instance) failed" +} + +make-summary() { + local dir=$1 + local impl=$2 + + local filename=results.html + + tests/make_summary.py $dir $dir/rows.html + + pushd $dir >/dev/null + + cat ../../tests/regtest.html \ + | sed -e '/__TABLE_ROWS__/ r rows.html' -e "s/_IMPL_/$impl/g" \ + > $filename + + popd >/dev/null + + log "Wrote $dir/$filename" + log "URL: file://$PWD/$dir/$filename" +} + +test-error() { + local spec_regex=${1:-} + log "Some test cases failed" + if test -n "$spec_regex"; then + log "(Perhaps none matched pattern '$spec_regex')" + fi + # don't quit just yet + # exit 1 +} + +# Assuming the spec file, write a list of test case names (first column) with +# the instance ids (second column), where instance ids run from 1 to $1. +# Third column is impl. +_setup-test-instances() { + local instances=$1 + local impl=$2 + + while read line; do + for i in $(seq 1 $instances); do + read case_name _ <<< $line # extract the first token + echo $case_name $i $impl + done + done +} + +# Print the default number of parallel processes, which is max(#CPUs - 1, 1) +default-processes() { + processors=$(grep -c ^processor /proc/cpuinfo || echo 4) # Linux-specific + if test $processors -gt 1; then # leave one CPU for the OS + processors=$(expr $processors - 1) + fi + echo $processors +} + +# Args: +# spec_gen: A program to execute to generate the spec. +# spec_regex: A pattern selecting the subset of tests to run +# parallel: Whether the tests are run in parallel (T/F). Sequential +# runs log to the console; parallel runs log to files. +# impl: one of python, or cpp +# instances: A number of times each test case is run + +_run-tests() { + local spec_gen=$1 + local spec_regex="$2" # grep -E format on the spec, can be empty + local parallel=$3 + local impl=${4:-"cpp"} + local instances=${5:-1} + + local regtest_dir=$REGTEST_BASE_DIR/$impl + rm -r -f --verbose $regtest_dir + + mkdir --verbose -p $regtest_dir + + local func + local processors + + if test $parallel = F; then + func=_run-one-instance # output to the console + processors=1 + else + func=_run-one-instance-logged + # Let the user override with MAX_PROC, in case they don't have enough + # memory. + processors=${MAX_PROC:-$(default-processes)} + log "Running $processors parallel processes" + fi + + local cases_list=$regtest_dir/test-cases.txt + # Need -- for regexes that start with - + $spec_gen | grep -E -- "$spec_regex" > $cases_list + + # Generate parameters for all test cases. + cat $cases_list \ + | xargs -l -P $processors -- $0 _setup-one-case $impl \ + || test-error + + log "Done generating parameters for all test cases" + + local instances_list=$regtest_dir/test-instances.txt + _setup-test-instances $instances $impl < $cases_list > $instances_list + + cat $instances_list \ + | xargs -l -P $processors -- $0 $func || test-error + + log "Done running all test instances" + + make-summary $regtest_dir $impl +} + +# used for most tests +readonly REGTEST_SPEC=tests/regtest_spec.py + +# Run tests sequentially. NOTE: called by demo.sh. +run-seq() { + local spec_regex=${1:-'^r-'} # grep -E format on the spec + shift + + time _run-tests $REGTEST_SPEC $spec_regex F $@ +} + +# Run tests in parallel +run() { + local spec_regex=${1:-'^r-'} # grep -E format on the spec + shift + + time _run-tests $REGTEST_SPEC $spec_regex T $@ +} + +# Run tests in parallel (7+ minutes on 8 cores) +run-all() { + log "Running all tests. Can take a while." + time _run-tests $REGTEST_SPEC '^r-' T cpp +} + +run-user() { + local spec_regex=${1:-} + local parallel=T # too much memory + time _run-tests tests/user_spec.py "$spec_regex" $parallel cpp +} + +# Use stable true values +compare-python-cpp() { + local num_unique_values=100 + local num_clients=10000 + local values_per_client=10 + local num_cohorts=64 + + local true_values=$REGTEST_BASE_DIR/stable_true_values.csv + + tests/gen_true_values.R \ + exp $num_unique_values $num_clients $values_per_client $num_cohorts \ + $true_values + + wc -l $true_values + + # Run Python and C++ simulation on the same input + + ./build.sh cpp-client + + TRUE_VALUES_PATH=$true_values \ + ./regtest.sh run-seq '^demo3' 1 python + + TRUE_VALUES_PATH=$true_values \ + ./regtest.sh run-seq '^demo3' 1 cpp + + head _tmp/{python,cpp}/demo3/1/case_reports.csv +} + +if test $# -eq 0 ; then + usage +else + "$@" +fi |