aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorErlend <erlendniko@hotmail.com>2022-08-11 13:24:41 +0200
committerErlend <erlendniko@hotmail.com>2022-08-11 13:24:41 +0200
commit5d94611148044c32ef24408ee6935ab2ce664e82 (patch)
treee18f34065ca51ed2ea3c5e6adf90118e967ddb01
parenta3db1c61f4da93b024eba736d22dffef66d66afe (diff)
parentacfbe53b1c03d7dcdc2d6947970de8d351ae8059 (diff)
Merge remote-tracking branch 'upstream/master'
-rw-r--r--client/README.md32
-rw-r--r--client/js/app/README.md51
-rw-r--r--clustercontroller-core/src/test/java/com/yahoo/vespa/clustercontroller/core/MasterElectionTest.java2
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/admin/monitoring/DefaultMetrics.java40
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java5
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java14
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java7
-rw-r--r--vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/TestRunnerHandler.java51
-rw-r--r--vespa-osgi-testrunner/src/test/resources/report.json71
-rw-r--r--vespajlib/src/main/java/ai/vespa/validation/PathValidator.java36
-rw-r--r--vespajlib/src/test/java/ai/vespa/validation/PathValidatorTest.java35
11 files changed, 180 insertions, 164 deletions
diff --git a/client/README.md b/client/README.md
index a459ea65b1d..8403e02a485 100644
--- a/client/README.md
+++ b/client/README.md
@@ -1,19 +1,45 @@
<!-- Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+![Vespa logo](https://vespa.ai/assets/vespa-logo-color.png)
+
# Vespa clients
+This part of the Vespa repository got Vespa client implementations for operations like
+* deploy
+* read/write
+* query
+
+<!-- ToDo: illustration -->
+
+
## Vespa CLI
The Vespa command-line tool, see the [README](go/README.md).
+Use the Vespa CLI to deploy, feed and query a Vespa application,
+for local, self-hosted or [Vespa Cloud](https://cloud.vespa.ai/) instances.
+
+
+
+## pyvespa
+[pyvespa](https://pyvespa.readthedocs.io/) provides a python API to Vespa -
+use it to create, modify, deploy and interact with running Vespa instances.
+The main pyvespa goal is to allow for faster prototyping
+and to facilitate Machine Learning experiments for Vespa applications.
+
## Vespa FE (fixme: better name and description here)
-This is a work-in-progress javascript app for various use cases.
+This is a [work-in-progress javascript app](js/app) for querying a Vespa application.
+
-## vespa_query_dsl
+----
+
+## Misc
+
+<!-- ToDo: move this / demote this somehow -->
+### vespa_query_dsl
This lib is used for composing Vespa
[YQL queries](https://docs.vespa.ai/en/reference/query-language-reference.html).
-
For usage, refer to the [QTest.java](src/test/java/ai/vespa/client/dsl/QTest.java) unit test.
ToDos:
diff --git a/client/js/app/README.md b/client/js/app/README.md
index 99271629edf..ae6a8d1cc25 100644
--- a/client/js/app/README.md
+++ b/client/js/app/README.md
@@ -1,48 +1,57 @@
<!-- Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+![Vespa logo](https://vespa.ai/assets/vespa-logo-color.png)
+
# Vespa client
This app is work in progress.
+It currently contains the **Query Builder** and the **Trace Visualizer**.
+
-This client currently contains the **Query Builder** and the **Trace Visualizer**.
-# Query Builder
+## Query Builder
+The Query Builder is a tool for creating Vespa queries to send to a local backend.
+The tool provides all of the options for query parameters from dropdowns.
+The input fields provide hints to what is the expected type of value.
-The Query Builder is a tool for creating Vespa queries to send to a local backend.
-The tool provides all of the options for query parameters from dropdowns. The input fields
-provide hints to what is the expected type of value.
-# Trace Visualizer
-The Trace Visualizer is a tool for converting and visualizing traces from Vespa in a flame graph.
-To use the visualizer, a [Jaeger](https://www.jaegertracing.io/) instance must be run locally with Docker.
+## Trace Visualizer
+The Trace Visualizer is a tool for converting and visualizing traces from Vespa in a flame graph.
+To use the visualizer, a [Jaeger](https://www.jaegertracing.io/) instance must be run locally with Docker:
docker run -d --rm \
- -p 16685:16685 \
- -p 16686:16686 \
- -p 16687:16687 \
- -e SPAN_STORAGE_TYPE=memory \
- jaegertracing/jaeger-query:latest
+ -p 16685:16685 \
+ -p 16686:16686 \
+ -p 16687:16687 \
+ -e SPAN_STORAGE_TYPE=memory \
+ jaegertracing/jaeger-query:latest
The Jaeger UI can then be reached at **localhost:16686/search**
-To use the visualizer you paste the Vespa trace into the text box and press the button to convert the trace
-to a format supported by Jaeger and download it.
-Only Vespa traces using _trace.timestampa=true_ **and** _traceLevel_ between 3 and 5 (inclusive) will work correctly.
+To use the visualizer,
+paste the Vespa trace into the text box
+and press the button to convert the trace to a format supported by Jaeger and download it.
+Only traces using _trace.timestamps=true_ **and** _traceLevel_ between 3 and 5 (inclusive) will work correctly -
+see [query tracing](https://docs.vespa.ai/en/query-api.html#query-tracing):
![Trace Converter](img/TraceConverter.png)
-After downloading the converted trace is can be used with the Jaeger UI.
-Press the _JSON File_ button as shown in the image, and drag and drop the trace you just downloaded.
+After downloading the converted trace, it can be used with the Jaeger UI.
+Press the _JSON File_ button as shown in the image, and drag and drop the trace you just downloaded:
-![Jager Image](img/JaegerExample.png)
+![Jaeger Image](img/JaegerExample.png)
-You can then click on the newly added trace and see it displayed as a flame graph.
+Then click on the newly added trace and see it displayed as a flame graph:
![Example Image](img/result.png)
-# Client install and start
+
+
+## Client install and start
nvm install --lts node # in case your current node.js is too old
yarn install
yarn dev # then open link, like http://127.0.0.1:3000/
+
+<!-- ToDo: publish a Docker image with all the clients ... -->
diff --git a/clustercontroller-core/src/test/java/com/yahoo/vespa/clustercontroller/core/MasterElectionTest.java b/clustercontroller-core/src/test/java/com/yahoo/vespa/clustercontroller/core/MasterElectionTest.java
index ae7ffd248d6..fb468ee4d5b 100644
--- a/clustercontroller-core/src/test/java/com/yahoo/vespa/clustercontroller/core/MasterElectionTest.java
+++ b/clustercontroller-core/src/test/java/com/yahoo/vespa/clustercontroller/core/MasterElectionTest.java
@@ -12,6 +12,7 @@ import com.yahoo.vdslib.state.NodeState;
import com.yahoo.vdslib.state.NodeType;
import com.yahoo.vdslib.state.State;
import com.yahoo.vespa.clustercontroller.core.status.StatusHandler;
+import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -290,6 +291,7 @@ public class MasterElectionTest extends FleetControllerTest {
}
@Test
+ @Disabled("Unstable, disable test, as functionality is not deemed critical")
void testMasterZooKeeperCooldown() throws Exception {
startingTest("MasterElectionTest::testMasterZooKeeperCooldown");
FleetControllerOptions options = defaultOptions("mycluster");
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/monitoring/DefaultMetrics.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/monitoring/DefaultMetrics.java
index 1798409d3d0..2348970ed1a 100644
--- a/config-model/src/main/java/com/yahoo/vespa/model/admin/monitoring/DefaultMetrics.java
+++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/monitoring/DefaultMetrics.java
@@ -55,11 +55,20 @@ public class DefaultMetrics {
Set<Metric> metrics = new LinkedHashSet<>();
metrics.add(new Metric("queries.rate"));
- metrics.add(new Metric("query_latency.average"));
+ metrics.add(new Metric("query_latency.sum"));
+ metrics.add(new Metric("query_latency.count"));
+ metrics.add(new Metric("query_latency.max"));
+ metrics.add(new Metric("query_latency.average")); // TODO: Remove with Vespa 9
metrics.add(new Metric("query_latency.95percentile"));
metrics.add(new Metric("query_latency.99percentile"));
- metrics.add(new Metric("hits_per_query.average"));
- metrics.add(new Metric("totalhits_per_query.average"));
+ metrics.add(new Metric("hits_per_query.sum"));
+ metrics.add(new Metric("hits_per_query.count"));
+ metrics.add(new Metric("hits_per_query.max"));
+ metrics.add(new Metric("hits_per_query.average")); // TODO: Remove with Vespa 9
+ metrics.add(new Metric("totalhits_per_query.sum"));
+ metrics.add(new Metric("totalhits_per_query.count"));
+ metrics.add(new Metric("totalhits_per_query.max"));
+ metrics.add(new Metric("totalhits_per_query.average")); // TODO: Remove with Vespa 9
metrics.add(new Metric("degraded_queries.rate"));
metrics.add(new Metric("failed_queries.rate"));
metrics.add(new Metric("serverActiveThreads.average"));
@@ -71,8 +80,14 @@ public class DefaultMetrics {
Set<Metric> metrics = new LinkedHashSet<>();
metrics.add(new Metric("content.proton.search_protocol.docsum.requested_documents.rate"));
- metrics.add(new Metric("content.proton.search_protocol.docsum.latency.average"));
- metrics.add(new Metric("content.proton.search_protocol.query.latency.average"));
+ metrics.add(new Metric("content.proton.search_protocol.docsum.latency.sum"));
+ metrics.add(new Metric("content.proton.search_protocol.docsum.latency.count"));
+ metrics.add(new Metric("content.proton.search_protocol.docsum.latency.max"));
+ metrics.add(new Metric("content.proton.search_protocol.docsum.latency.average")); // TODO: Remove with Vespa 9
+ metrics.add(new Metric("content.proton.search_protocol.query.latency.sum"));
+ metrics.add(new Metric("content.proton.search_protocol.query.latency.count"));
+ metrics.add(new Metric("content.proton.search_protocol.query.latency.max"));
+ metrics.add(new Metric("content.proton.search_protocol.query.latency.average")); // TODO: Remove with Vespa 9
metrics.add(new Metric("content.proton.documentdb.documents.total.last"));
metrics.add(new Metric("content.proton.documentdb.documents.ready.last"));
@@ -85,9 +100,18 @@ public class DefaultMetrics {
metrics.add(new Metric("content.proton.documentdb.matching.docs_matched.rate"));
metrics.add(new Metric("content.proton.documentdb.matching.docs_reranked.rate"));
- metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.query_setup_time.average"));
- metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.query_latency.average"));
- metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.rerank_time.average"));
+ metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.query_setup_time.sum"));
+ metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.query_setup_time.count"));
+ metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.query_setup_time.max"));
+ metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.query_setup_time.average")); // TODO: Remove with Vespa 9
+ metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.query_latency.sum"));
+ metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.query_latency.count"));
+ metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.query_latency.max"));
+ metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.query_latency.average")); // TODO: Remove with Vespa 9
+ metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.rerank_time.sum"));
+ metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.rerank_time.count"));
+ metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.rerank_time.max"));
+ metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.rerank_time.average")); // TODO: Remove with Vespa 9
metrics.add(new Metric("content.proton.transactionlog.disk_usage.last"));
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java
index 423d82687a3..f6174e1740f 100644
--- a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java
@@ -551,7 +551,10 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> {
cluster.setSearch(buildSearch(deployState, cluster, searchElement));
addSearchHandler(cluster, searchElement);
- addGUIHandler(cluster);
+
+ // Set up GUI handler only on self hosted
+ if (!deployState.isHosted())
+ addGUIHandler(cluster);
validateAndAddConfiguredComponents(deployState, cluster, searchElement, "renderer", ContainerModelBuilder::validateRendererElement);
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java
index 38d7b6d3a2b..ef3474e0c1e 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java
@@ -656,10 +656,13 @@ public class InternalStepRunner implements StepRunner {
controller.jobController().updateTestReport(id);
return Optional.of(testFailure);
case INCONCLUSIVE:
- long sleepMinutes = Math.max(15, Math.min(120, Duration.between(deployment.get().at(), controller.clock().instant()).toMinutes() / 20));
- logger.log("Tests were inconclusive, and will run again in " + sleepMinutes + " minutes.");
controller.jobController().updateTestReport(id);
- controller.jobController().locked(id, run -> run.sleepingUntil(controller.clock().instant().plusSeconds(60 * sleepMinutes)));
+ controller.jobController().locked(id, run -> {
+ Instant nextAttemptAt = run.start();
+ while ( ! nextAttemptAt.isAfter(controller.clock().instant())) nextAttemptAt = nextAttemptAt.plusSeconds(1800);
+ logger.log("Tests were inconclusive, and will run again at " + nextAttemptAt + ".");
+ return run.sleepingUntil(nextAttemptAt);
+ });
return Optional.of(reset);
case ERROR:
logger.log(INFO, "Tester failed running its tests!");
@@ -806,6 +809,7 @@ public class InternalStepRunner implements StepRunner {
Consumer<String> updater = msg -> controller.notificationsDb().setNotification(source, Notification.Type.deployment, Notification.Level.error, msg);
switch (run.status()) {
case aborted: return; // wait and see how the next run goes.
+ case noTests:
case running:
case success:
controller.notificationsDb().removeNotification(source, Notification.Type.deployment);
@@ -822,10 +826,6 @@ public class InternalStepRunner implements StepRunner {
case testFailure:
updater.accept("one or more verification tests against the deployment failed. Please review test output in the deployment job log.");
return;
- case noTests:
- controller.notificationsDb().setNotification(source, Notification.Type.deployment, Notification.Level.warning,
- "no tests were found for this job type. Please review test output in the deployment job log.");
- return;
case error:
case endpointCertificateTimeout:
break;
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java
index bf488198126..493c0945ecc 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java
@@ -358,7 +358,8 @@ public class InternalStepRunnerTest {
// Test sleeps for a while.
tester.runner().run();
assertEquals(unfinished, tester.jobs().run(id).stepStatuses().get(Step.deployTester));
- tester.clock().advance(Duration.ofSeconds(899));
+ Instant nextAttemptAt = tester.clock().instant().plusSeconds(1800);
+ tester.clock().advance(Duration.ofSeconds(1799));
tester.runner().run();
assertEquals(unfinished, tester.jobs().run(id).stepStatuses().get(Step.deployTester));
@@ -380,8 +381,8 @@ public class InternalStepRunnerTest {
assertTestLogEntries(id, Step.endTests,
new LogEntry(lastId1 + 1, Instant.ofEpochMilli(123), info, "Not enough data!"),
- new LogEntry(lastId1 + 2, instant1, info, "Tests were inconclusive, and will run again in 15 minutes."),
- new LogEntry(lastId1 + 15, instant1, info, "### Run will reset, and start over at " + instant1.plusSeconds(900).truncatedTo(SECONDS)),
+ new LogEntry(lastId1 + 2, instant1, info, "Tests were inconclusive, and will run again at " + nextAttemptAt + "."),
+ new LogEntry(lastId1 + 15, instant1, info, "### Run will reset, and start over at " + nextAttemptAt),
new LogEntry(lastId1 + 16, instant1, info, ""),
new LogEntry(lastId2 + 1, tester.clock().instant(), info, "Tests completed successfully."));
diff --git a/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/TestRunnerHandler.java b/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/TestRunnerHandler.java
index ef4b402d33b..756c3f55ab3 100644
--- a/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/TestRunnerHandler.java
+++ b/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/TestRunnerHandler.java
@@ -150,61 +150,10 @@ public class TestRunnerHandler extends ThreadedHttpRequestHandler {
json.writeFieldName("report");
render(json, (Node) report.root());
- // TODO jonmv: remove
- json.writeObjectFieldStart("summary");
-
- renderSummary(json, report);
-
- json.writeArrayFieldStart("failures");
- renderFailures(json, report.root());
- json.writeEndArray();
-
- json.writeEndObject();
-
- // TODO jonmv: remove
- json.writeArrayFieldStart("output");
- renderOutput(json, report.root());
- json.writeEndArray();
-
json.writeEndObject();
json.close();
}
- private static void renderSummary(JsonGenerator json, TestReport report) throws IOException {
- Map<TestReport.Status, Long> tally = report.root().tally();
- json.writeNumberField("success", tally.getOrDefault(TestReport.Status.successful, 0L));
- json.writeNumberField("failed", tally.getOrDefault(TestReport.Status.failed, 0L) + tally.getOrDefault(TestReport.Status.error, 0L));
- json.writeNumberField("ignored", tally.getOrDefault(TestReport.Status.skipped, 0L));
- json.writeNumberField("aborted", tally.getOrDefault(TestReport.Status.aborted, 0L));
- json.writeNumberField("inconclusive", tally.getOrDefault(TestReport.Status.inconclusive, 0L));
- }
-
- private static void renderFailures(JsonGenerator json, Node node) throws IOException {
- if (node instanceof FailureNode) {
- json.writeStartObject();
- json.writeStringField("testName", node.parent.name());
- json.writeStringField("testError", ((FailureNode) node).thrown().getMessage());
- json.writeStringField("exception", ExceptionUtils.getStackTraceAsString(((FailureNode) node).thrown()));
- json.writeEndObject();
- }
- else {
- for (Node child : node.children())
- renderFailures(json, child);
- }
- }
-
- private static void renderOutput(JsonGenerator json, Node node) throws IOException {
- if (node instanceof OutputNode) {
- for (LogRecord record : ((OutputNode) node).log())
- if (record.getMessage() != null)
- json.writeString(formatter.format(record.getInstant().atOffset(ZoneOffset.UTC)) + " " + record.getMessage());
- }
- else {
- for (Node child : node.children())
- renderOutput(json, child);
- }
- }
-
private static void render(JsonGenerator json, Node node) throws IOException {
json.writeStartObject();
if (node instanceof NamedNode) render(json, (NamedNode) node);
diff --git a/vespa-osgi-testrunner/src/test/resources/report.json b/vespa-osgi-testrunner/src/test/resources/report.json
index 443694e2e0c..66ae6dd398c 100644
--- a/vespa-osgi-testrunner/src/test/resources/report.json
+++ b/vespa-osgi-testrunner/src/test/resources/report.json
@@ -482,74 +482,5 @@
]
}
]
- },
- "summary": {
- "success": 3,
- "failed": 5,
- "ignored": 4,
- "aborted": 1,
- "inconclusive": 1,
- "failures": [
- {
- "testName": "error()",
- "testError": null,
- "exception": "java.lang.NoClassDefFoundError\n\tat com.yahoo.vespa.test.samples.SampleTest.error(SampleTest.java:87)\n"
- },
- {
- "testName": "failing()",
- "testError": "baz ==> expected: <foo> but was: <bar>",
- "exception": "org.opentest4j.AssertionFailedError: baz ==> expected: <foo> but was: <bar>\n\tat org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:55)\n\tat org.junit.jupiter.api.AssertionUtils.failNotEqual(AssertionUtils.java:62)\n\tat org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:182)\n\tat org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:1152)\n\tat com.yahoo.vespa.test.samples.SampleTest.failing(SampleTest.java:81)\n"
- },
- {
- "testName": "inconclusive(TestReporter)",
- "testError": "the cat is both dead _and_ alive",
- "exception": "ai.vespa.hosted.cd.InconclusiveTestException: the cat is both dead _and_ alive\n\tat com.yahoo.vespa.test.samples.SampleTest.inconclusive(SampleTest.java:93)\n"
- },
- {
- "testName": "third",
- "testError": "no charm",
- "exception": "org.opentest4j.AssertionFailedError: no charm\n\tat org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:39)\n\tat org.junit.jupiter.api.Assertions.fail(Assertions.java:134)\n\tat com.yahoo.vespa.test.samples.SampleTest$Inner.lambda$others$1(SampleTest.java:105)\n"
- },
- {
- "testName": "test()",
- "testError": "",
- "exception": "org.opentest4j.AssertionFailedError\n\tat org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:35)\n\tat org.junit.jupiter.api.Assertions.fail(Assertions.java:115)\n\tat com.yahoo.vespa.test.samples.FailingTestAndBothAftersTest.test(FailingTestAndBothAftersTest.java:19)\n\tSuppressed: java.lang.RuntimeException\n\t\tat com.yahoo.vespa.test.samples.FailingTestAndBothAftersTest.moreFail(FailingTestAndBothAftersTest.java:16)\n"
- },
- {
- "testName": "FailingTestAndBothAftersTest",
- "testError": null,
- "exception": "java.lang.RuntimeException\n\tat com.yahoo.vespa.test.samples.FailingTestAndBothAftersTest.fail(FailingTestAndBothAftersTest.java:13)\n"
- },
- {
- "testName": "WrongBeforeAllTest",
- "testError": "@BeforeAll method 'void com.yahoo.vespa.test.samples.WrongBeforeAllTest.wrong()' must be static unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS).",
- "exception": "org.junit.platform.commons.JUnitException: @BeforeAll method 'void com.yahoo.vespa.test.samples.WrongBeforeAllTest.wrong()' must be static unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS).\n"
- },
- {
- "testName": "test()",
- "testError": null,
- "exception": "java.lang.NullPointerException\n\tat com.yahoo.vespa.test.samples.FailingExtensionTest$FailingExtension.<init>(FailingExtensionTest.java:19)\n"
- },
- {
- "testName": "Production test",
- "testError": "School's out all summer!",
- "exception": "java.lang.ClassNotFoundException: School's out all summer!\n"
- }
- ]
- },
- "output": [
- "00:00:00.000 spam",
- "00:00:00.000 spam",
- "00:00:00.000 spam",
- "00:00:00.000 I have a bad feeling about this",
- "00:00:00.000 spam",
- "00:00:00.000 I'm here with Erwin today; Erwin, what can you tell us about your cat?",
- "00:00:00.000 spam",
- "00:00:00.000 <body />",
- "00:00:00.000 Very informative: \"\\n\": \n",
- "00:00:00.000 Oh no",
- "00:00:00.000 spam",
- "00:00:00.000 Catch me if you can!",
- "00:00:00.000 spam"
- ]
+ }
}
diff --git a/vespajlib/src/main/java/ai/vespa/validation/PathValidator.java b/vespajlib/src/main/java/ai/vespa/validation/PathValidator.java
new file mode 100644
index 00000000000..0ae81e2315d
--- /dev/null
+++ b/vespajlib/src/main/java/ai/vespa/validation/PathValidator.java
@@ -0,0 +1,36 @@
+package ai.vespa.validation;
+
+import java.nio.file.Path;
+
+/**
+ * Path validations
+ *
+ * @author mortent
+ */
+public class PathValidator {
+
+ /**
+ * Validate that file is a child of basedir
+ * @param root Root directory to use for validation
+ * @param path Path to validate
+ * @throws IllegalArgumentException if path is not a child of root
+ */
+ public static void validateChildOf(Path root, Path path) {
+ if (!path.normalize().startsWith(root)) {
+ throw new IllegalArgumentException("Invalid path %s".formatted(path));
+ }
+ }
+
+ /**
+ * Resolves a path under a root path
+ * @param root root poth
+ * @param path child to resolve
+ * @return The resolved path
+ * @throws IllegalArgumentException If the provided child path does not resolve as child of root
+ */
+ public static Path resolveChildOf(Path root, String path) {
+ Path resolved = root.resolve(path);
+ validateChildOf(root, resolved);
+ return resolved;
+ }
+}
diff --git a/vespajlib/src/test/java/ai/vespa/validation/PathValidatorTest.java b/vespajlib/src/test/java/ai/vespa/validation/PathValidatorTest.java
new file mode 100644
index 00000000000..a2c1bd6bd0c
--- /dev/null
+++ b/vespajlib/src/test/java/ai/vespa/validation/PathValidatorTest.java
@@ -0,0 +1,35 @@
+package ai.vespa.validation;
+
+import org.junit.Test;
+
+import java.nio.file.Path;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class PathValidatorTest {
+
+ @Test
+ public void testPathValidation() {
+ Path root = Path.of("/foo/");
+
+ assertOk(Path.of("/foo/bar"), root);
+ assertOk(Path.of("/foo/foo2/bar"), root);
+ assertOk(Path.of("/foo/foo2/../bar"), root);
+ assertOk(Path.of("/foo/../foo/bar"), root);
+ assertOk(Path.of("/bar/../foo/../foo/bar"), root);
+
+ assertInvalid(Path.of("/foo/../bar"), root);
+ assertInvalid(Path.of("/foo/bar/../../bar"), root);
+ }
+
+ private void assertOk(Path path, Path root) {
+ PathValidator.validateChildOf(root, path);
+ }
+
+ private void assertInvalid(Path path, Path root) {
+ IllegalArgumentException illegalArgumentException = assertThrows(IllegalArgumentException.class,
+ () -> PathValidator.validateChildOf(root, path));
+ assertEquals("Invalid path %s".formatted(path), illegalArgumentException.getMessage());
+ }
+}