1#!/usr/bin/env bash 2 3ROOT_MODULE="go.etcd.io/etcd" 4 5if [[ "$(go list)" != "${ROOT_MODULE}/v3" ]]; then 6 echo "must be run from '${ROOT_MODULE}/v3' module directory" 7 exit 255 8fi 9 10function set_root_dir { 11 ETCD_ROOT_DIR=$(go list -f '{{.Dir}}' "${ROOT_MODULE}/v3") 12} 13 14set_root_dir 15 16#### Convenient IO methods ##### 17 18COLOR_RED='\033[0;31m' 19COLOR_ORANGE='\033[0;33m' 20COLOR_GREEN='\033[0;32m' 21COLOR_LIGHTCYAN='\033[0;36m' 22COLOR_BLUE='\033[0;94m' 23COLOR_MAGENTA='\033[95m' 24COLOR_BOLD='\033[1m' 25COLOR_NONE='\033[0m' # No Color 26 27 28function log_error { 29 >&2 echo -n -e "${COLOR_BOLD}${COLOR_RED}" 30 >&2 echo "$@" 31 >&2 echo -n -e "${COLOR_NONE}" 32} 33 34function log_warning { 35 >&2 echo -n -e "${COLOR_ORANGE}" 36 >&2 echo "$@" 37 >&2 echo -n -e "${COLOR_NONE}" 38} 39 40function log_callout { 41 >&2 echo -n -e "${COLOR_LIGHTCYAN}" 42 >&2 echo "$@" 43 >&2 echo -n -e "${COLOR_NONE}" 44} 45 46function log_cmd { 47 >&2 echo -n -e "${COLOR_BLUE}" 48 >&2 echo "$@" 49 >&2 echo -n -e "${COLOR_NONE}" 50} 51 52function log_success { 53 >&2 echo -n -e "${COLOR_GREEN}" 54 >&2 echo "$@" 55 >&2 echo -n -e "${COLOR_NONE}" 56} 57 58function log_info { 59 >&2 echo -n -e "${COLOR_NONE}" 60 >&2 echo "$@" 61 >&2 echo -n -e "${COLOR_NONE}" 62} 63 64# From http://stackoverflow.com/a/12498485 65function relativePath { 66 # both $1 and $2 are absolute paths beginning with / 67 # returns relative path to $2 from $1 68 local source=$1 69 local target=$2 70 71 local commonPart=$source 72 local result="" 73 74 while [[ "${target#$commonPart}" == "${target}" ]]; do 75 # no match, means that candidate common part is not correct 76 # go up one level (reduce common part) 77 commonPart="$(dirname "$commonPart")" 78 # and record that we went back, with correct / handling 79 if [[ -z $result ]]; then 80 result=".." 81 else 82 result="../$result" 83 fi 84 done 85 86 if [[ $commonPart == "/" ]]; then 87 # special case for root (no common path) 88 result="$result/" 89 fi 90 91 # since we now have identified the common part, 92 # compute the non-common part 93 local forwardPart="${target#$commonPart}" 94 95 # and now stick all parts together 96 if [[ -n $result ]] && [[ -n $forwardPart ]]; then 97 result="$result$forwardPart" 98 elif [[ -n $forwardPart ]]; then 99 # extra slash removal 100 result="${forwardPart:1}" 101 fi 102 103 echo "$result" 104} 105 106#### Discovery of files/packages within a go module ##### 107 108# go_srcs_in_module [package] 109# returns list of all not-generated go sources in the current (dir) module. 110function go_srcs_in_module { 111 go fmt -n "$1" | grep -Eo "([^ ]*)$" | grep -vE "(\\_test.go|\\.pb\\.go|\\.pb\\.gw.go)" 112} 113 114# pkgs_in_module [optional:package_pattern] 115# returns list of all packages in the current (dir) module. 116# if the package_pattern is given, its being resolved. 117function pkgs_in_module { 118 go list -mod=mod "${1:-./...}"; 119} 120 121# Prints subdirectory (from the repo root) for the current module. 122function module_subdir { 123 relativePath "${ETCD_ROOT_DIR}" "${PWD}" 124} 125 126#### Running actions against multiple modules #### 127 128# run [command...] - runs given command, printing it first and 129# again if it failed (in RED). Use to wrap important test commands 130# that user might want to re-execute to shorten the feedback loop when fixing 131# the test. 132function run { 133 local rpath 134 local command 135 rpath=$(module_subdir) 136 # Quoting all components as the commands are fully copy-parsable: 137 command=("${@}") 138 command=("${command[@]@Q}") 139 if [[ "${rpath}" != "." && "${rpath}" != "" ]]; then 140 repro="(cd ${rpath} && ${command[*]})" 141 else 142 repro="${command[*]}" 143 fi 144 145 log_cmd "% ${repro}" 146 "${@}" 2> >(while read -r line; do echo -e "${COLOR_NONE}stderr: ${COLOR_MAGENTA}${line}${COLOR_NONE}">&2; done) 147 local error_code=$? 148 if [ ${error_code} -ne 0 ]; then 149 log_error -e "FAIL: (code:${error_code}):\\n % ${repro}" 150 return ${error_code} 151 fi 152} 153 154# run_for_module [module] [cmd] 155# executes given command in the given module for given pkgs. 156# module_name - "." (in future: tests, client, server) 157# cmd - cmd to be executed - that takes package as last argument 158function run_for_module { 159 local module=${1:-"."} 160 shift 1 161 ( 162 cd "${ETCD_ROOT_DIR}/${module}" && "$@" 163 ) 164} 165 166function module_dirs() { 167 echo "api pkg raft client/pkg client/v2 client/v3 server etcdutl etcdctl tests ." 168} 169 170# maybe_run [cmd...] runs given command depending on the DRY_RUN flag. 171function maybe_run() { 172 if ${DRY_RUN}; then 173 log_warning -e "# DRY_RUN:\\n % ${*}" 174 else 175 run "${@}" 176 fi 177} 178 179function modules() { 180 modules=( 181 "${ROOT_MODULE}/api/v3" 182 "${ROOT_MODULE}/pkg/v3" 183 "${ROOT_MODULE}/raft/v3" 184 "${ROOT_MODULE}/client/pkg/v3" 185 "${ROOT_MODULE}/client/v2" 186 "${ROOT_MODULE}/client/v3" 187 "${ROOT_MODULE}/server/v3" 188 "${ROOT_MODULE}/etcdutl/v3" 189 "${ROOT_MODULE}/etcdctl/v3" 190 "${ROOT_MODULE}/tests/v3" 191 "${ROOT_MODULE}/v3") 192 echo "${modules[@]}" 193} 194 195function modules_exp() { 196 for m in $(modules); do 197 echo -n "${m}/... " 198 done 199} 200 201# run_for_modules [cmd] 202# run given command across all modules and packages 203# (unless the set is limited using ${PKG} or / ${USERMOD}) 204function run_for_modules { 205 local pkg="${PKG:-./...}" 206 if [ -z "${USERMOD:-}" ]; then 207 for m in $(module_dirs); do 208 run_for_module "${m}" "$@" "${pkg}" || return "$?" 209 done 210 else 211 run_for_module "${USERMOD}" "$@" "${pkg}" || return "$?" 212 fi 213} 214 215 216#### Running go test ######## 217 218# go_test [packages] [mode] [flags_for_package_func] [$@] 219# [mode] supports 3 states: 220# - "parallel": fastest as concurrently processes multiple packages, but silent 221# till the last package. See: https://github.com/golang/go/issues/2731 222# - "keep_going" : executes tests package by package, but postpones reporting error to the last 223# - "fail_fast" : executes tests packages 1 by 1, exits on the first failure. 224# 225# [flags_for_package_func] is a name of function that takes list of packages as parameter 226# and computes additional flags to the go_test commands. 227# Use 'true' or ':' if you dont need additional arguments. 228# 229# depends on the VERBOSE top-level variable. 230# 231# Example: 232# go_test "./..." "keep_going" ":" --short 233# 234# The function returns != 0 code in case of test failure. 235function go_test { 236 local packages="${1}" 237 local mode="${2}" 238 local flags_for_package_func="${3}" 239 240 shift 3 241 242 local goTestFlags="" 243 local goTestEnv="" 244 if [ "${VERBOSE}" == "1" ]; then 245 goTestFlags="-v" 246 fi 247 248 # Expanding patterns (like ./...) into list of packages 249 250 local unpacked_packages=("${packages}") 251 if [ "${mode}" != "parallel" ]; then 252 # shellcheck disable=SC2207 253 # shellcheck disable=SC2086 254 if ! unpacked_packages=($(go list ${packages})); then 255 log_error "Cannot resolve packages: ${packages}" 256 return 255 257 fi 258 fi 259 260 local failures="" 261 262 # execution of tests against packages: 263 for pkg in "${unpacked_packages[@]}"; do 264 local additional_flags 265 # shellcheck disable=SC2086 266 additional_flags=$(${flags_for_package_func} ${pkg}) 267 268 # shellcheck disable=SC2206 269 local cmd=( go test ${goTestFlags} ${additional_flags} "$@" ${pkg} ) 270 271 # shellcheck disable=SC2086 272 if ! run env ${goTestEnv} "${cmd[@]}" ; then 273 if [ "${mode}" != "keep_going" ]; then 274 return 2 275 else 276 failures=("${failures[@]}" "${pkg}") 277 fi 278 fi 279 done 280 281 if [ -n "${failures[*]}" ] ; then 282 log_error -e "ERROR: Tests for following packages failed:\\n ${failures[*]}" 283 return 2 284 fi 285} 286 287#### Other #### 288 289# tool_exists [tool] [instruction] 290# Checks whether given [tool] is installed. In case of failure, 291# prints a warning with installation [instruction] and returns !=0 code. 292# 293# WARNING: This depend on "any" version of the 'binary' that might be tricky 294# from hermetic build perspective. For go binaries prefer 'tool_go_run' 295function tool_exists { 296 local tool="${1}" 297 local instruction="${2}" 298 if ! command -v "${tool}" >/dev/null; then 299 log_warning "Tool: '${tool}' not found on PATH. ${instruction}" 300 return 255 301 fi 302} 303 304# Ensure gobin is available, as it runs majority of the tools 305if ! command -v "gobin" >/dev/null; then 306 run env GO111MODULE=off go get github.com/myitcv/gobin || exit 1 307fi 308 309# tool_get_bin [tool] - returns absolute path to a tool binary (or returns error) 310function tool_get_bin { 311 tool_exists "gobin" "GO111MODULE=off go get github.com/myitcv/gobin" || return 2 312 313 local tool="$1" 314 if [[ "$tool" == *"@"* ]]; then 315 # shellcheck disable=SC2086 316 run gobin ${GOBINARGS:-} -p "${tool}" || return 2 317 else 318 # shellcheck disable=SC2086 319 run_for_module ./tools/mod run gobin ${GOBINARGS:-} -p -m --mod=readonly "${tool}" || return 2 320 fi 321} 322 323# tool_pkg_dir [pkg] - returns absolute path to a directory that stores given pkg. 324# The pkg versions must be defined in ./tools/mod directory. 325function tool_pkg_dir { 326 run_for_module ./tools/mod run go list -f '{{.Dir}}' "${1}" 327} 328 329# tool_get_bin [tool] 330function run_go_tool { 331 local cmdbin 332 if ! cmdbin=$(tool_get_bin "${1}"); then 333 return 2 334 fi 335 shift 1 336 run "${cmdbin}" "$@" || return 2 337} 338 339# assert_no_git_modifications fails if there are any uncommited changes. 340function assert_no_git_modifications { 341 log_callout "Making sure everything is committed." 342 if ! git diff --cached --exit-code; then 343 log_error "Found staged by uncommited changes. Do commit/stash your changes first." 344 return 2 345 fi 346 if ! git diff --exit-code; then 347 log_error "Found unstaged and uncommited changes. Do commit/stash your changes first." 348 return 2 349 fi 350} 351 352# makes sure that the current branch is in sync with the origin branch: 353# - no uncommitted nor unstaged changes 354# - no differencing commits in relation to the origin/$branch 355function git_assert_branch_in_sync { 356 local branch 357 branch=$(run git rev-parse --abbrev-ref HEAD) 358 # TODO: When git 2.22 popular, change to: 359 # branch=$(git branch --show-current) 360 if [[ $(run git status --porcelain --untracked-files=no) ]]; then 361 log_error "The workspace in '$(pwd)' for branch: ${branch} has uncommitted changes" 362 log_error "Consider cleaning up / renaming this directory or (cd $(pwd) && git reset --hard)" 363 return 2 364 fi 365 if [ -n "${branch}" ]; then 366 ref_local=$(run git rev-parse "${branch}") 367 ref_origin=$(run git rev-parse "origin/${branch}") 368 if [ "x${ref_local}" != "x${ref_origin}" ]; then 369 log_error "In workspace '$(pwd)' the branch: ${branch} diverges from the origin." 370 log_error "Consider cleaning up / renaming this directory or (cd $(pwd) && git reset --hard origin/${branch})" 371 return 2 372 fi 373 else 374 log_warning "Cannot verify consistency with the origin, as git is on detached branch." 375 fi 376} 377