diff --git a/.github/workflows/helm-chart-ci.yml b/.github/workflows/helm-chart-ci.yml index 5ba0135a21..d60495fbf6 100644 --- a/.github/workflows/helm-chart-ci.yml +++ b/.github/workflows/helm-chart-ci.yml @@ -349,6 +349,21 @@ jobs: expect "$(printf 'review\tsecret')" 'must be printable ASCII' expect 'café' 'must be printable ASCII' + - name: helm test query assertions and shell quoting + run: | + # Execute the rendered query with the same Groovy version as Server. + GROOVY_VERSION=$(python3 - <<'PY_VERSION' + import xml.etree.ElementTree as ET + pom = ET.parse('pom.xml') + print(pom.find('{*}properties/{*}groovy.version').text) + PY_VERSION + ) + GROOVY_URL="https://repo.maven.apache.org/maven2/org/apache/groovy/groovy/${GROOVY_VERSION}/groovy-${GROOVY_VERSION}.jar" + curl -fsSLo /tmp/groovy.jar "${GROOVY_URL}" + curl -fsSLo /tmp/groovy.jar.sha1 "${GROOVY_URL}.sha1" + printf '%s /tmp/groovy.jar\n' "$(cat /tmp/groovy.jar.sha1)" | sha1sum -c - + GROOVY_JAR=/tmp/groovy.jar python3 helm/test_helm_hook.py + - name: helm test hook behaviour shell: bash run: | diff --git a/helm/hugegraph/README.md b/helm/hugegraph/README.md index a65039558f..2a47137479 100644 --- a/helm/hugegraph/README.md +++ b/helm/hugegraph/README.md @@ -231,9 +231,16 @@ an existing account; follow [credential rotation](docs/operations.md#credentials The chart checks persisted Raft identities and storage/membership changes during upgrades; those changes require a coordinated procedure. -`helm --wait` checks readiness; `helm test` checks graph-bound Gremlin on each -Server Pod. Also verify a fresh Hubble login and graph query. Templates and -linting alone do not prove live volume, network or application behavior. +`helm --wait` checks readiness. `helm test` checks graph binding and visible +associated indexes for `CREATED` status on each Server Pod. Run it after index +creation/rebuild jobs finish, with no schema changes during the test. On HStore, +reading an unfinished index can cache its temporary status on other Pods; +retries do not refresh that state, and indexed queries may keep failing until +those Pods restart after the jobs finish. It does not prove index query results; add +[business assertions](docs/operations.md#business-query-assertions) +with `--set-file server.testGremlin=assertions.groovy`. Also verify a fresh Hubble +login and graph query. Templates and linting alone do not prove live volume, +network or application behavior. Website guides: [Helm deployment](https://hugegraph.apache.org/docs/quickstart/hugegraph/hugegraph-helm/) and [Kubernetes operations](https://hugegraph.apache.org/docs/quickstart/hugegraph/hugegraph-helm-operations/). diff --git a/helm/hugegraph/docs/operations.md b/helm/hugegraph/docs/operations.md index cb7f2e8048..693f4585af 100644 --- a/helm/hugegraph/docs/operations.md +++ b/helm/hugegraph/docs/operations.md @@ -283,6 +283,51 @@ and `q=1.` are equivalent to `q=0` and `q=1`. Explicit gzip exclusion overrides Older images can still force gzip; use a Server image built from current source. Updating chart values alone does not change an existing image's behavior. +## Business query assertions + +The default `helm test` checks the `server.testGraphAlias` binding (`DEFAULT-hugegraph` by default) +and requires `CREATED` indexes +associated with the vertex/edge labels visible on each Pod. It reads those +indexes by ID, using the same local cache path as queries; it does not refresh +the full index cache. This does not detect missing/stale label associations, +orphaned indexes or incorrect index data, and does not prove range-query results. +Run it after index creation/rebuild jobs finish, with no schema changes during +the test. Like indexed queries, the check rejects every other status, including +`CREATING`, `REBUILDING` and `INVALID`. On HStore, reading an unfinished index can +cache its temporary status on Pods other than the job runner. Later attempts +reuse that cache; the retry window does not repair it. Indexed queries on those +Pods may keep failing after the job succeeds; replace affected Server Pods only +after the jobs finish. This check does not fix cross-Server schema cache consistency. + +For a graph with existing test data, provide a read-only Gremlin script that +asserts the expected result. For example, with 40 existing `person` vertices +(ages 0–39), save this as `assertions.groovy`, adapting names/results to your data: + +```groovy +def n = graph.traversal().V().hasLabel('person') + .has('age', org.apache.tinkerpop.gremlin.process.traversal.P.gte(20)) + .count().next() +assert n == 20 : "Expected 20 adults, got ${n}" +``` + +```bash +helm upgrade hugegraph ./helm/hugegraph -n hugegraph --reuse-values \ + --set-file server.testGremlin=assertions.groovy +helm test hugegraph -n hugegraph --logs --timeout 5m +``` + +In each attempt, the hook checks binding/index state on every Ready Server Pod, +then runs optional business assertions on Pods where that check succeeds. A failing +request or assertion reports its Pod IP, check stage and response, and fails +the hook if no complete attempt succeeds before its deadline. Use +`kubectl -n hugegraph get pods -o wide` to map IPs to Pod names. Returning a value +or `false` alone is not a failure: use `assert` or throw an exception. Without a +script, the hook explicitly reports business assertions as skipped. The hook +creates no schema/data by default. Custom scripts use the hook's Server credential +(`admin` when authentication is enabled); read-only execution is not enforced, +so supply trusted, read-only assertions. The hook does not guarantee the first request +after startup succeeds; its retries allow transient elections to settle. + ## Troubleshooting | Symptom | First checks | diff --git a/helm/hugegraph/templates/tests/test-connection.yaml b/helm/hugegraph/templates/tests/test-connection.yaml index 08b7501c13..020c484f6a 100644 --- a/helm/hugegraph/templates/tests/test-connection.yaml +++ b/helm/hugegraph/templates/tests/test-connection.yaml @@ -16,6 +16,18 @@ # {{ include "hugegraph.resolveValues" . }} +{{- $smoke := `def ids = [] as Set; +(graph.schema().getVertexLabels() + graph.schema().getEdgeLabels()).each { + ids.addAll(it.indexLabels()) +}; +ids.each { + def index = graph.indexLabel(it); + if (!index.status().ok()) { + throw new IllegalStateException('Index ' + index.name() + ' is ' + + index.status().name() + ' (CREATED expected)') + } +}; +graph.traversal().V().limit(1).count()` }} # Official Helm chart test hook — only runs with: helm test # Not an install-time hook. Safe with --wait (no init Job in no-init design). apiVersion: v1 @@ -78,7 +90,12 @@ spec: PORT="{{ .Values.server.port }}" FLOOR="{{ include "hugegraph.server.replicaFloor" . }}" DEADLINE_S="${HG_TEST_DEADLINE_S:-150}" - BODY={{ dict "gremlin" "graph.traversal().V().limit(1).count()" "aliases" (dict "graph" (.Values.server.testGraphAlias | default "DEFAULT-hugegraph")) | toJson | squote }} + BODY='{{ dict "gremlin" $smoke "aliases" (dict "graph" (.Values.server.testGraphAlias | default "DEFAULT-hugegraph")) | toJson | replace "'" "'\"'\"'" }}' + {{- if .Values.server.testGremlin }} + BUSINESS_BODY='{{ dict "gremlin" .Values.server.testGremlin "aliases" (dict "graph" (.Values.server.testGraphAlias | default "DEFAULT-hugegraph")) | toJson | replace "'" "'\"'\"'" }}' + {{- else }} + BUSINESS_BODY="" + {{- end }} {{- if .Values.server.auth.enabled }} AUTH_HEADER=$(printf 'admin:%s' "${PASSWORD}" | base64 | tr -d '\r\n') request() { @@ -88,7 +105,7 @@ spec: gremlin() { printf 'header = "Authorization: Basic %s"\n' "${AUTH_HEADER}" | curl --config - -sS --compressed --connect-timeout 3 --max-time 20 \ - -H 'Content-Type: application/json' -X POST -d "${BODY}" \ + -H 'Content-Type: application/json' -X POST -d "$2" \ -w '\n%{http_code}\n' "$1" 2>&1 } {{- else }} @@ -97,10 +114,21 @@ spec: } gremlin() { curl -sS --compressed --connect-timeout 3 --max-time 20 \ - -H 'Content-Type: application/json' -X POST -d "${BODY}" \ + -H 'Content-Type: application/json' -X POST -d "$2" \ -w '\n%{http_code}\n' "$1" 2>&1 } {{- end }} + check_gremlin() { + OUT=$(gremlin "$1" "$3") || true + CODE=$(printf '%s\n' "${OUT}" | grep -E '^[0-9][0-9][0-9]$' | tail -1) || CODE="" + if [ "${CODE}" = "200" ]; then + echo "helm test: $2 OK on $1" + return 0 + fi + printf 'helm test: %s failed on %s code=%s body: %.400s\n' \ + "$2" "$1" "${CODE:-no-response}" "${OUT}" + return 1 + } echo "helm test: GET http://${SVC}:${PORT}/versions" request "http://${SVC}:${PORT}/versions" echo @@ -115,6 +143,9 @@ spec: # Pods, retrying until the deadline so a transient PD election is # not misread as a broken Pod. echo "helm test: Gremlin on every Server Pod via ${HEADLESS} (floor ${FLOOR})" + if [ -z "${BUSINESS_BODY}" ]; then + echo "helm test: business assertions skipped (server.testGremlin is empty)" + fi START=$(date +%s) while :; do # The headless Service answers with every Ready Pod's addresses, @@ -135,18 +166,16 @@ spec: FAILED="" for IP in ${IPS}; do case "${IP}" in *:*) TARGET="[${IP}]" ;; *) TARGET="${IP}" ;; esac - OUT=$(gremlin "http://${TARGET}:${PORT}/gremlin") || true - CODE=$(printf '%s\n' "${OUT}" | grep -E '^[0-9][0-9][0-9]$' | tail -1) || CODE="" - if [ "${CODE}" = "200" ]; then - echo "helm test: Gremlin OK on ${IP}" - else + URL="http://${TARGET}:${PORT}/gremlin" + if ! check_gremlin "${URL}" "Gremlin binding/index-state" "${BODY}"; then + FAILED="${FAILED}${IP}(${CODE:-no-response}) " + elif [ -n "${BUSINESS_BODY}" ] && + ! check_gremlin "${URL}" "business assertion" "${BUSINESS_BODY}"; then FAILED="${FAILED}${IP}(${CODE:-no-response}) " - printf 'helm test: Gremlin failed on %s code=%s body: %.400s\n' \ - "${IP}" "${CODE:-no-response}" "${OUT}" fi done if [ -z "${FAILED}" ] && [ "${COUNT}" -ge "${FLOOR}" ]; then - echo "helm test: OK (${COUNT} Server Pods, all Gremlin-bound)" + echo "helm test: OK (${COUNT} Server Pods, all configured checks passed)" exit 0 fi if [ $(( $(date +%s) - START )) -ge "${DEADLINE_S}" ]; then diff --git a/helm/hugegraph/tests/test_hook_gremlin_test.yaml b/helm/hugegraph/tests/test_hook_gremlin_test.yaml index abe2e97606..1e40618b98 100644 --- a/helm/hugegraph/tests/test_hook_gremlin_test.yaml +++ b/helm/hugegraph/tests/test_hook_gremlin_test.yaml @@ -91,6 +91,19 @@ tests: path: spec.containers[0].command[2] pattern: '"graph":"DEFAULT-Demo"' + - it: defaults a null alias for both index and business checks + template: tests/test-connection.yaml + set: + server.testGraphAlias: null + server.testGremlin: "assert graph.traversal().V().count().next() == 40" + asserts: + - matchRegex: + path: spec.containers[0].command[2] + pattern: '(?m)^\s*BODY=.*"graph":"DEFAULT-hugegraph"' + - matchRegex: + path: spec.containers[0].command[2] + pattern: '(?m)^\s*BUSINESS_BODY=.*"graph":"DEFAULT-hugegraph"' + - it: floors the per-Pod count at the HPA minimum when HPA is enabled template: tests/test-connection.yaml set: @@ -102,6 +115,40 @@ tests: path: spec.containers[0].command[2] pattern: "FLOOR=\"5\"" + - it: checks linked indexes by ID without refreshing the full index cache + template: tests/test-connection.yaml + asserts: + - matchRegex: + path: spec.containers[0].command[2] + pattern: "getVertexLabels.*getEdgeLabels" + - matchRegex: + path: spec.containers[0].command[2] + pattern: "graph.indexLabel" + - matchRegex: + path: spec.containers[0].command[2] + pattern: "index.status.*ok" + - matchRegex: + path: spec.containers[0].command[2] + pattern: "CREATED expected" + - notMatchRegex: + path: spec.containers[0].command[2] + pattern: "getIndexLabels" + - matchRegex: + path: spec.containers[0].command[2] + pattern: "business assertions skipped" + + - it: sends optional business assertions on each Pod + template: tests/test-connection.yaml + set: + server.testGremlin: "assert graph.traversal().V().count().next() == 40" + asserts: + - matchRegex: + path: spec.containers[0].command[2] + pattern: "assert graph.traversal" + - matchRegex: + path: spec.containers[0].command[2] + pattern: "business assertion" + - it: sends no credentials when authentication is disabled template: tests/test-connection.yaml set: diff --git a/helm/hugegraph/values.schema.json b/helm/hugegraph/values.schema.json index 3d5248e3d8..987e650c16 100644 --- a/helm/hugegraph/values.schema.json +++ b/helm/hugegraph/values.schema.json @@ -1173,6 +1173,10 @@ "testResources": { "$ref": "#/definitions/resources" }, + "testGremlin": { + "type": "string", + "description": "Optional Gremlin assertions run with the hook's Server credential on each Pod. Supply trusted read-only scripts; read-only execution is not enforced." + }, "service": { "$ref": "#/definitions/service" }, diff --git a/helm/hugegraph/values.yaml b/helm/hugegraph/values.yaml index e029446cf6..af635efeca 100644 --- a/helm/hugegraph/values.yaml +++ b/helm/hugegraph/values.yaml @@ -349,6 +349,10 @@ server: # Existing Gremlin graph binding checked on every Server Pod. Select the # target graph explicitly; graphSpace does not create or infer this binding. testGraphAlias: DEFAULT-hugegraph + # Optional Gremlin assertions, run with the hook's Server credential on every Pod. + # Supply trusted read-only scripts; read-only execution is not enforced. + # Use --set-file server.testGremlin=/path/to/assertions.groovy; assert expected results. + testGremlin: "" # Resources for the Helm test hook container. Bounded by default so the hook # cannot run unlimited on a restricted or quota-managed namespace. testResources: diff --git a/helm/test_helm_hook.py b/helm/test_helm_hook.py new file mode 100644 index 0000000000..8c3a2d30f4 --- /dev/null +++ b/helm/test_helm_hook.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +"""Execute the rendered Helm hook with local command fixtures, without networking.""" + +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +import unittest + +import yaml + + +ROOT = Path(__file__).resolve().parents[1] +CHART = ROOT / "helm" / "hugegraph" +MOCK = r''' +import json, os, pathlib, sys +command = pathlib.Path(sys.argv[0]).name +directory = pathlib.Path(os.environ["FIXTURE_DIR"]) +if command == "date": + counter = directory / "time" + value = int(counter.read_text()) + 100 if counter.exists() else 0 + counter.write_text(str(value)) + print(value) +elif command == "getent": + for address in os.environ["FIXTURE_IPS"].split(): + print(address, "STREAM server") +elif command == "curl": + args = sys.argv[1:] + config = sys.stdin.read() if "--config" in args else "" + url = args[-1] + body = json.loads(args[args.index("-d") + 1]) if "-d" in args else None + with (directory / "requests.jsonl").open("a") as output: + output.write(json.dumps({"url": url, "body": body, + "args": args, "config": config}) + "\n") + if body is None: + print("{}") + else: + case = os.environ.get("FIXTURE_CASE", "healthy") + bad_pod = "10.0.0.2" in url + business = body["gremlin"] == os.environ.get("FIXTURE_BUSINESS") + failure = bad_pod and (case in ("binding", "invalid", "creating", "rebuilding") or + (case == "business" and business)) + if failure: + message = {"binding": "Could not rebind graph", + "invalid": "Index by_age is INVALID (CREATED expected)", + "creating": "Index by_age is CREATING (CREATED expected)", + "rebuilding": "Index by_age is REBUILDING (CREATED expected)", + "business": "Assertion failed: expected index results"}[case] + print(json.dumps({"message": message})) + print("400" if case == "binding" else "500") + else: + print('{"result":{"data":[0]}}\n200') +''' + + +class HelmHookTest(unittest.TestCase): + def render(self, directory, legacy_values=False, **values): + chart = CHART + if legacy_values: + # --reuse-values can render with old defaults that predate this key. + chart = directory / "legacy-chart" + shutil.copytree(CHART, chart) + defaults_file = chart / "values.yaml" + defaults = yaml.safe_load(defaults_file.read_text()) + defaults["server"].pop("testGraphAlias") + defaults_file.write_text(yaml.safe_dump(defaults)) + configuration = directory / "values.yaml" + configuration.write_text(yaml.safe_dump(values)) + result = subprocess.run( + ["helm", "template", "fixture", str(chart), "-f", str(configuration), + "--show-only", "templates/tests/test-connection.yaml"], + capture_output=True, text=True) + self.assertEqual(0, result.returncode, result.stderr) + return yaml.safe_load(result.stdout)["spec"]["containers"][0]["command"][2] + + def run_hook(self, case="healthy", addresses=None, legacy_values=False, **values): + temporary = tempfile.TemporaryDirectory(prefix="helm-hook-") + self.addCleanup(temporary.cleanup) + directory = Path(temporary.name) + script = self.render(directory, legacy_values=legacy_values, **values) + commands = directory / "bin" + commands.mkdir() + for name in ("curl", "date", "getent", "sleep"): + executable = commands / name + executable.write_text(f"#!{sys.executable}\n" + MOCK) + executable.chmod(0o755) + business = values.get("server", {}).get("testGremlin", "") + environment = dict(os.environ, PATH=f"{commands}:{os.environ['PATH']}", + FIXTURE_DIR=str(directory), FIXTURE_CASE=case, + FIXTURE_BUSINESS=business, PASSWORD="fixture-secret", + HG_TEST_DEADLINE_S="1", FIXTURE_IPS=addresses or + "10.0.0.1 10.0.0.2 10.0.0.3") + result = subprocess.run(["sh", "-c", script], env=environment, cwd=directory, + capture_output=True, text=True, timeout=15) + requests = [json.loads(line) for line in + (directory / "requests.jsonl").read_text().splitlines()] + return result, requests, directory + + def test_every_pod_is_checked_with_auth(self): + result, requests, _ = self.run_hook() + self.assertEqual(0, result.returncode, result.stdout + result.stderr) + self.assertEqual(3, sum(request["body"] is not None for request in requests)) + self.assertTrue(all("Authorization: Basic " in request["config"] for request in requests)) + self.assertIn("business assertions skipped", result.stdout) + + def test_failed_binding_or_unready_cached_index_reports_pod_and_stage(self): + for case, message in (("binding", "Could not rebind graph"), + ("invalid", "Index by_age is INVALID (CREATED expected)"), + ("creating", "Index by_age is CREATING (CREATED expected)"), + ("rebuilding", "Index by_age is REBUILDING (CREATED expected)")): + with self.subTest(case=case): + result, requests, _ = self.run_hook(case) + self.assertNotEqual(0, result.returncode) + self.assertIn("Gremlin binding/index-state failed on http://10.0.0.2:8080/gremlin", + result.stdout) + self.assertIn(message, result.stdout) + self.assertEqual(3, sum(request["body"] is not None for request in requests)) + + def test_business_assertion_runs_on_every_pod_and_reports_failure(self): + business = "assert graph.traversal().V().count().next() == 7; true" + for case, expected in (("healthy", 0), ("business", 1)): + with self.subTest(case=case): + result, requests, _ = self.run_hook(case, server={"testGremlin": business}) + self.assertEqual(expected, result.returncode, result.stdout + result.stderr) + self.assertEqual(3, sum(request["body"] is not None and + request["body"]["gremlin"] == business for request in requests)) + if expected: + self.assertIn("business assertion failed on http://10.0.0.2:8080/gremlin", + result.stdout) + self.assertIn("Assertion failed: expected index results", result.stdout) + + def test_selected_alias_applies_to_index_and_business_checks(self): + business = "assert graph.traversal().V().count().next() == 7; true" + result, requests, _ = self.run_hook( + server={"testGraphAlias": "team_a-demo", "testGremlin": business}) + self.assertEqual(0, result.returncode, result.stdout + result.stderr) + payloads = [request["body"] for request in requests if request["body"]] + self.assertEqual(6, len(payloads)) + self.assertTrue(all(payload["aliases"] == {"graph": "team_a-demo"} + for payload in payloads)) + + def test_missing_or_null_alias_preserves_default_for_both_checks(self): + business = "assert graph.traversal().V().count().next() == 7; true" + for missing in (False, True): + with self.subTest(alias="missing" if missing else "null"): + server = {"testGremlin": business} + if not missing: + server["testGraphAlias"] = None + result, requests, _ = self.run_hook(legacy_values=missing, server=server) + self.assertEqual(0, result.returncode, result.stdout + result.stderr) + payloads = [request["body"] for request in requests if request["body"]] + self.assertEqual(6, len(payloads)) + self.assertEqual([{"graph": "DEFAULT-hugegraph"}] * 6, + [payload["aliases"] for payload in payloads]) + + def test_business_script_is_json_data_and_auth_can_be_disabled(self): + business = '''assert 'single' != "double" +// `touch injected` $(touch injected) ' " +true''' + result, requests, directory = self.run_hook( + server={"testGremlin": business, "auth": {"enabled": False}}, + hubble={"enabled": False}) + self.assertEqual(0, result.returncode, result.stdout + result.stderr) + self.assertFalse((directory / "injected").exists()) + self.assertTrue(all(not request["config"] and "--config" not in request["args"] + for request in requests)) + self.assertEqual(3, sum(request["body"] is not None and + request["body"]["gremlin"] == business for request in requests)) + + def test_dual_stack_does_not_count_pods_twice(self): + result, requests, _ = self.run_hook( + addresses="10.0.0.1 10.0.0.2 fd00::1 fd00::2") + self.assertNotEqual(0, result.returncode) + self.assertIn("pods=2 floor=3", result.stderr) + self.assertEqual(2, sum(request["body"] is not None for request in requests)) + + def test_rendered_groovy_checks_cached_ids_without_refreshing_indexes(self): + self.assertTrue(os.environ.get("GROOVY_JAR"), + "Set GROOVY_JAR to the Server Groovy jar before running this suite") + _, requests, directory = self.run_hook() + payload = next(request["body"] for request in requests if request["body"]) + script = directory / "smoke.groovy" + script.write_text(payload["gremlin"]) + result = subprocess.run( + [shutil.which("java"), "-cp", os.environ["GROOVY_JAR"], "groovy.ui.GroovyMain", + str(ROOT / "helm" / "testdata" / "test_index_check.groovy"), str(script)], + capture_output=True, text=True, timeout=30) + self.assertEqual(0, result.returncode, result.stdout + result.stderr) + self.assertIn("cached index checks passed", result.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/helm/testdata/test_index_check.groovy b/helm/testdata/test_index_check.groovy new file mode 100644 index 0000000000..90257b7360 --- /dev/null +++ b/helm/testdata/test_index_check.groovy @@ -0,0 +1,60 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You 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. + +// Execute the rendered script, with stale cached ID 11 but healthy metadata. +class FixtureTraversal { + def V() { this } + def limit(count) { assert count == 1; this } + def count() { 0 } +} + +class FixtureGraph { + Map statuses + List lookups = [] + def schema() { + return [getVertexLabels: { -> statuses.containsKey(11) ? [[indexLabels: { -> [11] }]] : [] }, + getEdgeLabels: { -> statuses.containsKey(22) ? [[indexLabels: { -> [22] }]] : [] }, + getIndexLabels: { -> throw new AssertionError('metadata enumeration refreshes cache') }] + } + def indexLabel(id) { + assert id instanceof Integer : 'name lookup can refresh the cache' + lookups.add(id) + return [name: { -> "index_${id}" }, + status: { -> [name: { -> statuses[id] }, ok: { -> statuses[id] == 'CREATED' }] }] + } + def traversal() { + return new FixtureTraversal() + } +} + +def source = new File(args[0]).text +[[11: 'CREATED', 22: 'CREATED'], [:]].each { statuses -> + def graph = new FixtureGraph(statuses: statuses) + assert new GroovyShell(new Binding(graph: graph)).evaluate(source) == 0 + assert graph.lookups as Set == statuses.keySet() +} +['CREATING', 'REBUILDING', 'DELETING', 'UNDELETED', 'INVALID', 'CLEARING'].each { status -> + [11, 22].each { indexId -> + def graph = new FixtureGraph(statuses: [11: 'CREATED', 22: 'CREATED']) + graph.statuses[indexId] = status + try { + new GroovyShell(new Binding(graph: graph)).evaluate(source) + throw new AssertionError('non-CREATED cached index must fail') + } catch (IllegalStateException expected) { + assert expected.message == "Index index_${indexId} is ${status} (CREATED expected)" + } + } +} +println 'cached index checks passed'