#!/usr/bin/env bash
# Copyright 2014 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This script verifies the following e2e test ownership policies
# - tests MUST start with [sig-foo]
# - tests SHOULD NOT have multiple [sig-foo] tags
# TODO: these two can be dropped if KubeDescribe is gone from codebase
# - tests MUST NOT have [k8s.io] in test names
# - tests MUST NOT use KubeDescribe
set -o errexit
set -o nounset
set -o pipefail
# This will canonicalize the path
KUBE_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd -P)
source "${KUBE_ROOT}/hack/lib/init.sh"
# Set REUSE_BUILD_OUTPUT=y to skip rebuilding dependencies if present
REUSE_BUILD_OUTPUT=${REUSE_BUILD_OUTPUT:-n}
# set VERBOSE_OUTPUT=y to output .jq files and shell commands
VERBOSE_OUTPUT=${VERBOSE_OUTPUT:-n}
if [[ ${VERBOSE_OUTPUT} =~ ^[yY]$ ]]; then
set -x
fi
pushd "${KUBE_ROOT}" > /dev/null
# Setup a tmpdir to hold generated scripts and results
tmpdir=$(mktemp -d -t verify-e2e-test-ownership.XXXX)
readonly tmpdir
trap 'rm -rf ${tmpdir}' EXIT
# input
spec_summaries="${KUBE_ROOT}/_output/specsummaries.json"
# output
results_json="${tmpdir}/results.json"
summary_json="${tmpdir}/summary.json"
failures_json="${tmpdir}/failures.json"
# rebuild dependencies if necessary
function ensure_dependencies() {
local -r ginkgo="${KUBE_ROOT}/_output/bin/ginkgo"
local -r e2e_test="${KUBE_ROOT}/_output/bin/e2e.test"
if ! { [ -f "${ginkgo}" ] && [[ "${REUSE_BUILD_OUTPUT}" =~ ^[yY]$ ]]; }; then
make ginkgo
fi
if ! { [ -f "${e2e_test}" ] && [[ "${REUSE_BUILD_OUTPUT}" =~ ^[yY]$ ]]; }; then
hack/make-rules/build.sh test/e2e/e2e.test
fi
if ! { [ -f "${spec_summaries}" ] && [[ "${REUSE_BUILD_OUTPUT}" =~ ^[yY]$ ]]; }; then
"${ginkgo}" --dry-run=true "${e2e_test}" -- --spec-dump "${spec_summaries}" > /dev/null
fi
}
# evaluate ginkgo spec summaries against e2e test ownership polices
# output to ${results_json}
function generate_results_json() {
readonly results_jq=${tmpdir}/results.jq
cat >"${results_jq}" <<EOS
[.[] | select( .LeafNodeType == "It") | . as { ContainerHierarchyTexts: \$text, ContainerHierarchyLocations: \$code, LeafNodeText: \$leafText, LeafNodeLocation: \$leafCode} | {
calls: ([ \$text | range(0;length) as \$i | {
sig: ((\$text[\$i] | match("\\\[(sig-[^\\\]]+)\\\]") | .captures[0].string) // "unknown"),
text: \$text[\$i],
# unused, but if we ever wanted to have policies based on other tags...
# tags: \$text[\$i] | [match("(\\\[[^\\\]]+\\\])"; "g").string],
line: \$code[\$i] | "\(.FileName):\(.LineNumber)"
}] + [{
sig: ((\$leafText | match("\\\[(sig-[^\\\]]+)\\\]") | .captures[0].string) // "unknown"),
text: \$leafText,
# unused, but if we ever wanted to have policies based on other tags...
# tags: \$leafText | [match("(\\\[[^\\\]]+\\\])"; "g").string],
line: \$leafCode | "\(.FileName):\(.LineNumber)"
}]),
} | {
owner: .calls[0].sig,
calls: .calls,
testname: .calls | map(.text) | join(" "),
policies: [(
.calls[0] |
{
fail: (.sig == "unknown"),
level: "FAIL",
category: "unowned_test",
reason: "must start with [sig-foo]",
found: .,
}
), (
.calls[1:] |
(map(select(.sig != "unknown")) // [] | {
fail: . | any,
level: "WARN",
category: "too_many_sigs",
reason: "should not have multiple [sig-foo] tags",
found: .,
})
)
]
}]
EOS
if [[ ${VERBOSE_OUTPUT} =~ ^[yY]$ ]]; then
echo "about to ${results_jq}..."
cat -n "${results_jq}"
echo
fi
<"${spec_summaries}" jq --slurp --from-file "${results_jq}" > "${results_json}"
}
# summarize e2e test policy results
# output to ${summary_json}
function generate_summary_json() {
summary_jq=${tmpdir}/summary.jq
cat >"${summary_jq}" <<EOS
. as \$results |
# for each policy category
reduce \$results[0].policies[] as \$p ({}; . + {
# add a convenience .policy field containing that policy's result
(\$p.category): \$results | map(. + {policy: .policies[] | select(.category == \$p.category)}) | {
level: \$p.level,
reason: \$p.reason,
passing: map(select(.policy.fail | not)) | length,
failing: map(select(.policy.fail)) | length,
testnames: map(select(.policy.fail) | .testname),
}
})
# add a meta policy based on whether any policy failed
+ {
all_policies: \$results | {
level: "WARN",
reason: "should pass all policies",
passing: map(select(.policies | map(.fail) | any | not)) | length,
failing: map(select(.policies | map(.fail) | any)) | length,
testnames: map(select(.policies | map(.fail) | any) | .testname),
}
}
# if a policy has no failing tests, change its log output to PASS
| with_entries(.value += { log: (if (.value.failing == 0) then "PASS" else .value.level end) })
# sort by policies with the most failing tests first
| to_entries | sort_by(.value.failing) | reverse | from_entries
EOS
if [[ ${VERBOSE_OUTPUT} =~ ^[yY]$ ]]; then
echo "about to run ${results_jq}..."
cat -n "${summary_jq}"
echo
fi
<"${results_json}" jq --from-file "${summary_jq}" > "${summary_json}"
}
# filter e2e policy tests results to tests that failed, with the policies they failed
# output to ${failures_json}
function generate_failures_json() {
local -r failures_jq="${tmpdir}/failures.jq"
cat >"${failures_jq}" <<EOS
.
# for each test
| map(
# filter down to failing policies; trim category, .reason is more verbose
.policies |= map(select(.fail) | del(.category))
# trim the full callstack, .found will contain the relevant call
| del(.calls)
)
# filter down to tests that have failed policies
| map(select(.policies | map (.fail) | any))
EOS
if [[ ${VERBOSE_OUTPUT} =~ ^[yY]$ ]]; then
echo "about to run ${failures_jq}..."
cat -n "${failures_jq}"
echo
fi
<"${results_json}" jq --from-file "${failures_jq}" > "${failures_json}"
}
function output_results_and_exit_if_failed() {
local -r total_tests=$(<"${spec_summaries}" wc -l | awk '{print $1}')
# output results to console
(
echo "run at datetime: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "based on commit: $(git log -n1 --date=iso-strict --pretty='%h - %cd - %s')"
echo
<"${failures_json}" cat
printf "%4s: e2e tests %-40s: %-4d\n" "INFO" "in total" "${total_tests}"
<"${summary_json}" jq -r 'to_entries[].value |
"printf \"%4s: ..failing %-40s: %-4d\\n\" \"\(.log)\" \"\(.reason)\" \"\(.failing)\""' | sh
) | tee "${tmpdir}/output.txt"
# if we said "FAIL" in that output, we should fail
if <"${tmpdir}/output.txt" grep -q "^FAIL"; then
echo "FAIL"
exit 1
fi
}
ensure_dependencies
generate_results_json
generate_failures_json
generate_summary_json
output_results_and_exit_if_failed
echo "PASS"