#!/usr/bin/env bash set -euo pipefail usage() { { echo "shard.sh: Shard tests for parallel execution in CI." echo "usage: $0 [-h] -n -m [-d ] [-s]" echo echo " -h: Show this help message." echo " -n: The shard number (1-indexed)." echo " -m: The total number of shards. Must be equal to or greater than -n." echo " -N: The shard in shard notation (n/m), corresponding to -n and -m." echo " -d: The directory to find packages with tests in." echo " Can be a path or a /... style pattern." echo " Can be repeated to specify multiple directories." echo " Can be - to read from stdin." echo " Default: ./..." echo " -s: Split final package list with spaces rather than newlines." } >&2 } is_int() { # we can't just return the result of the regex match shellcheck is unhappy... if [[ "$1" =~ ^[0-9]+$ ]]; then return 0 else return 1 fi } n=0 m=0 dirs=() s=0 while getopts ":hn:m:d:sN:" opt; do case $opt in h) usage exit 0 ;; n) if ! is_int "$OPTARG"; then echo "Error: -n must be an integer." >&2 usage exit 1 fi n=$OPTARG ;; m) if ! is_int "$OPTARG"; then echo "Error: -m must be an integer." >&2 usage exit 1 fi m=$OPTARG ;; N) if [[ "$OPTARG" =~ ^([0-9]+)/([0-9]+)$ ]]; then n="${BASH_REMATCH[1]}" m="${BASH_REMATCH[2]}" else echo "Error: -N must be in the form n/m." >&2 usage exit 1 fi ;; d) dirs+=("$OPTARG") ;; s) s=1 ;; \?) echo "Invalid option: -$OPTARG" >&2 usage exit 1 ;; :) echo "Option -$OPTARG requires an argument." >&2 usage exit 1 ;; esac done shift $((OPTIND - 1)) if [[ $n -eq 0 || $m -eq 0 ]]; then echo "Error: -n and -m are required." >&2 usage exit 1 fi if [[ $n -lt 1 || $m -lt 1 ]]; then echo "Error: -n and -m must be greater than 0." >&2 usage exit 1 fi if [[ $n -gt $m ]]; then echo "Error: -n must be less than or equal to -m." >&2 usage exit 1 fi if [[ ${#dirs[@]} -eq 0 ]]; then readarray -t dirs <<< "$(find . -type f -name 'go.mod' -exec dirname '{}' ';' | awk '{ print $1 "/..."; }')" fi # If dirs is just ("-"), read from stdin instead. if [[ ${#dirs[@]} -eq 1 && "${dirs[0]}" == "-" ]]; then dirs=() while IFS= read -r line; do dirs+=("$line") done fi if [[ $n -eq 1 && $m -eq 1 ]]; then # If there is only one shard, just return all packages. for pkg in "${dirs[@]}"; do if [ $s -eq 1 ]; then printf "%s " "$pkg" else printf "%s\n" "$pkg" fi done exit 0 fi readarray -t PACKAGES <<< "$(go list -f '{{.Dir}}' -e "${dirs[@]}")" if [[ ${#PACKAGES[@]} -eq 0 ]]; then echo "No packages found in directories: ${dirs[*]}" >&2 exit 1 fi for i in "${!PACKAGES[@]}"; do if [ -z "$(find "${PACKAGES[i]}" -maxdepth 1 -type f -name '*_test.go' -printf '.' -quit)" ]; then # There are no test files in this package. unset 'PACKAGES[i]' fi done for i in "${!PACKAGES[@]}"; do if (( (i % m) + 1 != n )); then unset 'PACKAGES[i]' fi done for pkg in "${PACKAGES[@]}"; do if [ $s -eq 1 ]; then printf "%s " "$pkg" else printf "%s\n" "$pkg" fi done