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