This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new a38641fd302a CAMEL-23688: camel-jbang - Fix infra ps/get for
hyphenated service names and add unit tests
a38641fd302a is described below
commit a38641fd302a8c5721bff03e464c07c584bce82c
Author: Adriano Machado <[email protected]>
AuthorDate: Fri Jun 26 01:45:15 2026 -0400
CAMEL-23688: camel-jbang - Fix infra ps/get for hyphenated service names
and add unit tests
Fix infra ps and infra get commands dropping hyphenated service names
(e.g. hive-mq) by consolidating pid-file parsing into shared
serviceNameFromPidFile/pidFromPidFile helpers that use indexOf/lastIndexOf
instead of split("-"). Add unit tests for DataWeave lexer/parser,
CatalogKamelet sorting, VersionSet config, and InfraGet/InfraPs.
Closes #24257
Co-Authored-By: Claude <[email protected]>
---
.../core/commands/infra/InfraBaseCommand.java | 30 ++-
.../dsl/jbang/core/commands/infra/InfraGet.java | 3 +-
.../dsl/jbang/core/commands/infra/InfraPs.java | 2 +-
.../core/commands/catalog/CatalogKameletTest.java | 140 ++++++++++
.../commands/config/BaseConfigTestSupport.java | 5 +-
.../commands/infra/InfraCommandTestSupport.java | 68 +++++
.../jbang/core/commands/infra/InfraGetTest.java | 92 +++++++
.../dsl/jbang/core/commands/infra/InfraPsTest.java | 77 ++++++
.../commands/transform/DataWeaveLexerTest.java | 153 +++++++++++
.../commands/transform/DataWeaveParserTest.java | 286 +++++++++++++++++++++
.../core/commands/version/VersionSetTest.java | 124 +++++++++
11 files changed, 972 insertions(+), 8 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraBaseCommand.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraBaseCommand.java
index ec6e143772de..474cdb266f6a 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraBaseCommand.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraBaseCommand.java
@@ -52,7 +52,6 @@ import
org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
import org.apache.camel.dsl.jbang.core.common.TerminalWidthHelper;
import org.apache.camel.dsl.jbang.core.model.InfraBaseDTO;
import org.apache.camel.support.PatternHelper;
-import org.apache.camel.util.FileUtil;
import org.apache.camel.util.json.DeserializationException;
import org.apache.camel.util.json.Jsoner;
import picocli.CommandLine;
@@ -98,8 +97,8 @@ public abstract class InfraBaseCommand extends CamelCommand {
.toList();
for (Path pidFile : pidFiles) {
String fn = pidFile.getFileName().toString();
- String sn = fn.substring(fn.indexOf("-") + 1,
fn.lastIndexOf('-'));
- String pid = fn.substring(fn.lastIndexOf("-") + 1,
fn.lastIndexOf('.'));
+ String sn = serviceNameFromPidFile(fn);
+ String pid = pidFromPidFile(fn);
if (pid.equals(pattern) || PatternHelper.matchPattern(sn,
pattern)) {
pids.put(Long.valueOf(pid), pidFile);
}
@@ -111,6 +110,29 @@ public abstract class InfraBaseCommand extends
CamelCommand {
return pids;
}
+ /**
+ * Extracts the service name from an {@code infra-<service>-<pid>.json}
pid file name. The service name may itself
+ * contain hyphens (for example {@code hive-mq}), so it spans everything
between the first and the last hyphen
+ * rather than just the second hyphen-delimited segment.
+ *
+ * @param pidFileName the pid file name, such as {@code
infra-hive-mq-1234.json}.
+ * @return the service name, such as {@code hive-mq}.
+ */
+ protected static String serviceNameFromPidFile(String pidFileName) {
+ return pidFileName.substring(pidFileName.indexOf('-') + 1,
pidFileName.lastIndexOf('-'));
+ }
+
+ /**
+ * Extracts the pid from an {@code infra-<service>-<pid>.json} pid file
name. The pid is the segment between the
+ * last hyphen and the file extension, which is robust to service names
that themselves contain hyphens.
+ *
+ * @param pidFileName the pid file name, such as {@code
infra-hive-mq-1234.json}.
+ * @return the pid, such as {@code 1234}.
+ */
+ protected static String pidFromPidFile(String pidFileName) {
+ return pidFileName.substring(pidFileName.lastIndexOf('-') + 1,
pidFileName.lastIndexOf('.'));
+ }
+
protected boolean showPidColumn() {
return false;
}
@@ -223,7 +245,7 @@ public abstract class InfraBaseCommand extends CamelCommand
{
Files.createDirectories(p);
for (String s : Objects.requireNonNull(p.toFile().list())) {
if (s.startsWith("infra-" + key + "-") && s.endsWith(".json"))
{
- return FileUtil.stripExt(s.split("-")[2]);
+ return pidFromPidFile(s);
}
}
} catch (Exception e) {
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraGet.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraGet.java
index 9e4af0a1ba2a..705dc3734410 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraGet.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraGet.java
@@ -44,8 +44,7 @@ public class InfraGet extends InfraBaseCommand {
int i = 0;
for (var e : pids.entrySet()) {
Path pidFile = e.getValue();
- String fn = pidFile.getFileName().toString();
- String sn = fn.substring(fn.indexOf("-") + 1, fn.lastIndexOf('-'));
+ String sn =
serviceNameFromPidFile(pidFile.getFileName().toString());
if (i > 0) {
printer().println("");
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraPs.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraPs.java
index d1808324e456..f761be085a8b 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraPs.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraPs.java
@@ -62,7 +62,7 @@ public class InfraPs extends InfraBaseCommand {
.toList();
for (Path pidFile : pidFiles) {
- String runningServiceName =
pidFile.getFileName().toString().split("-")[1];
+ String runningServiceName =
serviceNameFromPidFile(pidFile.getFileName().toString());
runningAliases.add(runningServiceName);
}
} catch (IOException e) {
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKameletTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKameletTest.java
new file mode 100644
index 000000000000..9fc4d5dda497
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKameletTest.java
@@ -0,0 +1,140 @@
+/*
+ * 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.
+ */
+package org.apache.camel.dsl.jbang.core.commands.catalog;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelCommandBaseTestSupport;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Unit tests for the row sorting logic of {@link CatalogKamelet}.
+ *
+ * The {@code doCall()} method downloads the Kamelet catalog from Maven and is
therefore not unit tested here; the
+ * deterministic, user-visible ordering produced by {@link
CatalogKamelet#sortRow} is what these tests pin down.
+ */
+class CatalogKameletTest extends CamelCommandBaseTestSupport {
+
+ private static KameletModel model(String name, String type, String
supportLevel, String description) {
+ KameletModel m = new KameletModel();
+ m.name = name;
+ m.type = type;
+ m.supportLevel = supportLevel;
+ m.description = description;
+ return m;
+ }
+
+ private List<KameletModel> sampleRows() {
+ List<KameletModel> rows = new ArrayList<>();
+ // intentionally unordered, mixed case, so a no-op comparator would be
detectable
+ rows.add(model("kafka-source", "source", "Stable", "Reads from
Kafka"));
+ rows.add(model("Aws-S3-sink", "sink", "Preview", "Writes to S3"));
+ rows.add(model("timer-source", "action", "Stable", "A timer based
source"));
+ return rows;
+ }
+
+ private CatalogKamelet command() {
+ return new CatalogKamelet(new CamelJBangMain().withPrinter(printer));
+ }
+
+ private static List<String> names(List<KameletModel> rows) {
+ return rows.stream().map(r -> r.name).toList();
+ }
+
+ @Test
+ void shouldSortByNameCaseInsensitiveByDefault() {
+ CatalogKamelet cmd = command();
+ cmd.sort = "name";
+
+ List<KameletModel> rows = sampleRows();
+ rows.sort(cmd::sortRow);
+
+ // case-insensitive: "Aws-S3-sink" sorts before "kafka-source"
+ assertEquals(List.of("Aws-S3-sink", "kafka-source", "timer-source"),
names(rows));
+ }
+
+ @Test
+ void shouldReverseOrderWhenSortKeyHasLeadingMinus() {
+ CatalogKamelet cmd = command();
+ cmd.sort = "-name";
+
+ List<KameletModel> rows = sampleRows();
+ rows.sort(cmd::sortRow);
+
+ assertEquals(List.of("timer-source", "kafka-source", "Aws-S3-sink"),
names(rows));
+ }
+
+ @Test
+ void shouldSortByType() {
+ CatalogKamelet cmd = command();
+ cmd.sort = "type";
+
+ List<KameletModel> rows = sampleRows();
+ rows.sort(cmd::sortRow);
+
+ // ordered by type: action, sink, source
+ assertEquals(List.of("timer-source", "Aws-S3-sink", "kafka-source"),
names(rows));
+ }
+
+ @Test
+ void shouldSortBySupportLevelViaBothAliases() {
+ List<KameletModel> rowsLevel = sampleRows();
+ CatalogKamelet level = command();
+ level.sort = "level";
+ rowsLevel.sort(level::sortRow);
+
+ List<KameletModel> rowsSupportLevel = sampleRows();
+ CatalogKamelet supportLevel = command();
+ supportLevel.sort = "support-level";
+ rowsSupportLevel.sort(supportLevel::sortRow);
+
+ // "level" and "support-level" are documented aliases and must produce
identical ordering
+ assertEquals(names(rowsLevel), names(rowsSupportLevel));
+ // "Preview" sorts before "Stable"; the two Stable rows keep stable
relative order
+ assertEquals(List.of("Aws-S3-sink", "kafka-source", "timer-source"),
names(rowsLevel));
+ }
+
+ @Test
+ void shouldSortByDescription() {
+ CatalogKamelet cmd = command();
+ cmd.sort = "description";
+
+ List<KameletModel> rows = sampleRows();
+ rows.sort(cmd::sortRow);
+
+ assertEquals(List.of("timer-source", "kafka-source", "Aws-S3-sink"),
names(rows));
+ }
+
+ @Test
+ void shouldLeaveOrderUnchangedForUnknownSortKey() {
+ CatalogKamelet cmd = command();
+ cmd.sort = "bogus";
+
+ List<KameletModel> original = sampleRows();
+ List<KameletModel> rows = sampleRows();
+ rows.sort(cmd::sortRow);
+
+ // an unrecognized key yields a comparator that returns 0, so the
input order is preserved
+ assertEquals(names(original), names(rows));
+ assertEquals(0, cmd.sortRow(original.get(0), original.get(1)),
+ "unknown sort key must produce a no-op comparator");
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/config/BaseConfigTestSupport.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/config/BaseConfigTestSupport.java
index 909ced04975b..384aa8e2211e 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/config/BaseConfigTestSupport.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/config/BaseConfigTestSupport.java
@@ -27,7 +27,10 @@ import org.junit.jupiter.api.AfterEach;
public class BaseConfigTestSupport extends CamelCommandBaseTestSupport {
@AfterEach
- void removeLocalConfigFile() throws IOException {
+ void removeConfigFiles() throws IOException {
Files.deleteIfExists(Paths.get(CommandLineHelper.LOCAL_USER_CONFIG));
+ // remove the global config file written under target by
UserConfigHelper.createUserConfig, so it does not
+ // leak into later tests in the same JVM. Target the build directory
explicitly to never touch the real home.
+ Files.deleteIfExists(Paths.get("target",
CommandLineHelper.USER_CONFIG));
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraCommandTestSupport.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraCommandTestSupport.java
new file mode 100644
index 000000000000..8e587c3c4bd2
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraCommandTestSupport.java
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+package org.apache.camel.dsl.jbang.core.commands.infra;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Comparator;
+import java.util.stream.Stream;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelCommandBaseTestSupport;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+
+/**
+ * Base class for infra command tests that read the {@code
infra-<service>-<pid>.json} pid files from the Camel
+ * directory. It redirects the home directory to an isolated folder under
{@code target} and clears it between tests,
+ * following the same convention as {@code ProcessCommandTestSupport}.
+ */
+abstract class InfraCommandTestSupport extends CamelCommandBaseTestSupport {
+
+ @BeforeEach
+ @Override
+ public void setup() throws Exception {
+ super.setup();
+ CommandLineHelper.useHomeDir("target/test-infra-cmd");
+ Files.createDirectories(CommandLineHelper.getCamelDir());
+ }
+
+ @AfterEach
+ public void cleanup() throws Exception {
+ Path camelDir = CommandLineHelper.getCamelDir();
+ if (Files.exists(camelDir)) {
+ try (Stream<Path> walk = Files.walk(camelDir)) {
+ walk.sorted(Comparator.reverseOrder()).forEach(p -> {
+ try {
+ Files.deleteIfExists(p);
+ } catch (IOException e) {
+ // best effort cleanup
+ }
+ });
+ }
+ }
+ }
+
+ /**
+ * Writes a pid file named {@code infra-<service>-<pid>.json} into the
Camel directory with the given JSON content.
+ */
+ protected static void writePidFile(String service, long pid, String
content) throws IOException {
+ Path pidFile = CommandLineHelper.getCamelDir().resolve("infra-" +
service + "-" + pid + ".json");
+ Files.writeString(pidFile, content);
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraGetTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraGetTest.java
new file mode 100644
index 000000000000..a0ce688bd631
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraGetTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.
+ */
+package org.apache.camel.dsl.jbang.core.commands.infra;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for {@link InfraGet}.
+ *
+ * The command resolves running infra services from the pid files ({@code
infra-<service>-<pid>.json}) stored in the
+ * Camel directory, then prints a header plus the file contents for each
match. The home directory is redirected to an
+ * isolated folder by {@link InfraCommandTestSupport} so the tests are
independent of any real local services.
+ */
+class InfraGetTest extends InfraCommandTestSupport {
+
+ @Test
+ void shouldPrintServiceHeaderAndContentByName() throws Exception {
+ writePidFile("kafka", 1234, "{\"port\":9092}");
+
+ InfraGet command = new InfraGet(new
CamelJBangMain().withPrinter(printer));
+ command.name = "kafka";
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String out = printer.getOutput();
+ // the service name and pid are parsed back out of the file name
+ assertTrue(out.contains("Service kafka (PID: 1234)"), "should print
the service header, was: " + out);
+ assertTrue(out.contains("{\"port\":9092}"), "should print the pid file
contents, was: " + out);
+ }
+
+ @Test
+ void shouldMatchByExactPid() throws Exception {
+ writePidFile("kafka", 1234, "{\"port\":9092}");
+ writePidFile("ftp", 5678, "{\"port\":21}");
+
+ InfraGet command = new InfraGet(new
CamelJBangMain().withPrinter(printer));
+ command.name = "5678";
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String out = printer.getOutput();
+ // a numeric argument matches the pid, not the service name
+ assertTrue(out.contains("Service ftp (PID: 5678)"), "should match the
ftp service by pid, was: " + out);
+ assertFalse(out.contains("kafka"), "should not print the unmatched
kafka service, was: " + out);
+ }
+
+ @Test
+ void shouldParseHyphenatedServiceName() throws Exception {
+ // service names may contain hyphens; the whole name between first and
last hyphen must be recovered
+ writePidFile("hive-mq", 1234, "{\"port\":1883}");
+
+ InfraGet command = new InfraGet(new
CamelJBangMain().withPrinter(printer));
+ command.name = "hive-mq";
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String out = printer.getOutput();
+ assertTrue(out.contains("Service hive-mq (PID: 1234)"),
+ "should recover the full hyphenated service name, was: " +
out);
+ }
+
+ @Test
+ void shouldPrintNothingWhenNoServiceMatches() throws Exception {
+ writePidFile("kafka", 1234, "{\"port\":9092}");
+
+ InfraGet command = new InfraGet(new
CamelJBangMain().withPrinter(printer));
+ command.name = "redis";
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ assertEquals("", printer.getOutput(), "no matching service must
produce no output");
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraPsTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraPsTest.java
new file mode 100644
index 000000000000..3ae0a669890c
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraPsTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.
+ */
+package org.apache.camel.dsl.jbang.core.commands.infra;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for {@link InfraPs}.
+ *
+ * Unlike {@code infra list}, which prints every known service, {@code infra
ps} only shows the services that have a
+ * running pid file ({@code infra-<service>-<pid>.json}) in the Camel
directory, and includes the PID column. The home
+ * directory is redirected to an isolated folder by {@link
InfraCommandTestSupport} so the tests do not depend on
+ * locally running services.
+ */
+class InfraPsTest extends InfraCommandTestSupport {
+
+ @Test
+ void shouldOnlyListRunningServicesWithPid() throws Exception {
+ // kafka is running, minio is not
+ writePidFile("kafka", 1234, "{}");
+
+ InfraPs command = new InfraPs(new
CamelJBangMain().withPrinter(printer));
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String out = printer.getOutput();
+ assertTrue(out.contains("kafka"), "running kafka service should be
listed, was: " + out);
+ assertTrue(out.contains("1234"), "running kafka service should show
its pid, was: " + out);
+ assertFalse(out.contains("minio"), "non-running services must be
filtered out, was: " + out);
+ }
+
+ @Test
+ void shouldListRunningHyphenatedService() throws Exception {
+ // regression: the running alias is parsed with the full hyphenated
name (e.g. hive-mq), not just the
+ // second hyphen-delimited segment ("hive"), otherwise the catalog row
would be filtered out
+ writePidFile("hive-mq", 1234, "{}");
+
+ InfraPs command = new InfraPs(new
CamelJBangMain().withPrinter(printer));
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String out = printer.getOutput();
+ assertTrue(out.contains("hive-mq"), "running hyphenated service should
be listed, was: " + out);
+ assertTrue(out.contains("1234"), "running hyphenated service should
show its pid, was: " + out);
+ }
+
+ @Test
+ void shouldListNoServicesWhenNoneRunning() throws Exception {
+ // no pid files written: the service table must be cleared
+ InfraPs command = new InfraPs(new
CamelJBangMain().withPrinter(printer));
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String out = printer.getOutput();
+ assertFalse(out.contains("kafka"), "no service rows should be printed
when nothing is running, was: " + out);
+ assertFalse(out.contains("minio"), "no service rows should be printed
when nothing is running, was: " + out);
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/transform/DataWeaveLexerTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/transform/DataWeaveLexerTest.java
new file mode 100644
index 000000000000..9395ac07424c
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/transform/DataWeaveLexerTest.java
@@ -0,0 +1,153 @@
+/*
+ * 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.
+ */
+package org.apache.camel.dsl.jbang.core.commands.transform;
+
+import java.util.List;
+
+import org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveLexer.Token;
+import
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveLexer.TokenType;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Unit tests for {@link DataWeaveLexer}.
+ *
+ * These assert the concrete token stream (types, values and positions) for
representative inputs, focusing on the
+ * tricky, ambiguous cases where a tokenizer typically goes wrong:
multi-character operators, negative numbers vs the
+ * minus operator, the {@code ---} header separator, and comment skipping.
+ */
+class DataWeaveLexerTest {
+
+ private static List<Token> tokenize(String input) {
+ return new DataWeaveLexer(input).tokenize();
+ }
+
+ private static List<TokenType> types(String input) {
+ return tokenize(input).stream().map(Token::type).toList();
+ }
+
+ @Test
+ void shouldAlwaysAppendEofToken() {
+ List<Token> tokens = tokenize("");
+ assertEquals(1, tokens.size());
+ assertEquals(TokenType.EOF, tokens.get(0).type());
+ }
+
+ @Test
+ void shouldClassifyKeywordsAndIdentifiers() {
+ // true/false -> BOOLEAN, null -> NULL_LIT, and/or/not -> logical ops,
everything else -> IDENTIFIER
+ assertEquals(
+ List.of(TokenType.BOOLEAN, TokenType.BOOLEAN,
TokenType.NULL_LIT,
+ TokenType.AND, TokenType.OR, TokenType.NOT,
+ TokenType.IDENTIFIER, TokenType.EOF),
+ types("true false null and or not payload"));
+ }
+
+ @Test
+ void shouldReadStringWithBothQuoteStylesAndKeepEscapes() {
+ List<Token> tokens = tokenize("\"hello\" 'world' \"a\\\"b\"");
+ assertEquals(TokenType.STRING, tokens.get(0).type());
+ assertEquals("hello", tokens.get(0).value());
+ assertEquals(TokenType.STRING, tokens.get(1).type());
+ assertEquals("world", tokens.get(1).value());
+ // the escaped quote is preserved verbatim inside the string value
+ assertEquals("a\\\"b", tokens.get(2).value());
+ }
+
+ @Test
+ void shouldReadDecimalNumber() {
+ List<Token> tokens = tokenize("3.14");
+ assertEquals(TokenType.NUMBER, tokens.get(0).type());
+ assertEquals("3.14", tokens.get(0).value());
+ }
+
+ @Test
+ void shouldTreatLeadingMinusAsNegativeNumberWhenNotAfterValue() {
+ // at the start of input the minus binds to the number
+ List<Token> tokens = tokenize("-5");
+ assertEquals(TokenType.NUMBER, tokens.get(0).type());
+ assertEquals("-5", tokens.get(0).value());
+ }
+
+ @Test
+ void shouldTreatMinusBetweenValuesAsOperator() {
+ // "5 - 3": the minus follows a value, so it is the subtraction
operator, not a sign
+ assertEquals(
+ List.of(TokenType.NUMBER, TokenType.MINUS, TokenType.NUMBER,
TokenType.EOF),
+ types("5 - 3"));
+ }
+
+ @Test
+ void shouldTreatMinusAfterOpeningParenAsNegativeNumber() {
+ // "(-3)": previous char is '(', not value-like, so the minus binds to
the number
+ assertEquals(
+ List.of(TokenType.LPAREN, TokenType.NUMBER, TokenType.RPAREN,
TokenType.EOF),
+ types("(-3)"));
+ assertEquals("-3", tokenize("(-3)").get(1).value());
+ }
+
+ @Test
+ void shouldDistinguishSingleAndDoubleCharacterOperators() {
+ assertEquals(
+ List.of(TokenType.PLUSPLUS, TokenType.PLUS, TokenType.ARROW,
TokenType.MINUS,
+ TokenType.EQ, TokenType.ASSIGN, TokenType.NEQ,
TokenType.GE, TokenType.GT,
+ TokenType.LE, TokenType.LT, TokenType.EOF),
+ types("++ + -> - == = != >= > <= <"));
+ }
+
+ @Test
+ void shouldRecognizeHeaderSeparatorAtLineStart() {
+ // the --- on its own line is a header separator, while the negative
number afterwards stays a number
+ List<TokenType> ts = types("%dw 2.0\n---\n-5");
+ assertEquals(
+ List.of(TokenType.PERCENT, TokenType.IDENTIFIER,
TokenType.NUMBER,
+ TokenType.HEADER_SEPARATOR, TokenType.NUMBER,
TokenType.EOF),
+ ts);
+ }
+
+ @Test
+ void shouldSkipLineAndBlockComments() {
+ String input = """
+ // a line comment
+ payload /* inline block */ + 1
+ """;
+ assertEquals(
+ List.of(TokenType.IDENTIFIER, TokenType.PLUS,
TokenType.NUMBER, TokenType.EOF),
+ types(input));
+ }
+
+ @Test
+ void shouldTrackLineAndColumnPositions() {
+ // "ab\n cd": first token at 1:1, second at line 2 after two spaces
-> 2:3
+ List<Token> tokens = tokenize("ab\n cd");
+ assertEquals(1, tokens.get(0).line());
+ assertEquals(1, tokens.get(0).col());
+ assertEquals("ab", tokens.get(0).value());
+ assertEquals(2, tokens.get(1).line());
+ assertEquals(3, tokens.get(1).col());
+ assertEquals("cd", tokens.get(1).value());
+ }
+
+ @Test
+ void shouldSkipUnknownCharactersInsteadOfFailing() {
+ // '@' is not a recognized token; the lexer drops it and keeps going
+ assertEquals(
+ List.of(TokenType.IDENTIFIER, TokenType.IDENTIFIER,
TokenType.EOF),
+ types("a @ b"));
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/transform/DataWeaveParserTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/transform/DataWeaveParserTest.java
new file mode 100644
index 000000000000..8802531c2932
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/transform/DataWeaveParserTest.java
@@ -0,0 +1,286 @@
+/*
+ * 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.
+ */
+package org.apache.camel.dsl.jbang.core.commands.transform;
+
+import java.util.List;
+
+import
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.ArrayLit;
+import
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.BinaryOp;
+import
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.DefaultExpr;
+import
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.FieldAccess;
+import
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.FunctionCall;
+import
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.Identifier;
+import org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.IfElse;
+import
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.IndexAccess;
+import org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.Lambda;
+import
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.LambdaShorthand;
+import org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.MapExpr;
+import
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.NumberLit;
+import
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.ObjectLit;
+import org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.Parens;
+import org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.Script;
+import
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.StringLit;
+import
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.TypeCoercion;
+import org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.UnaryOp;
+import
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.Unsupported;
+import org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.VarDecl;
+import org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveLexer.Token;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for {@link DataWeaveParser}.
+ *
+ * The parser is recursive descent and best-effort (it never throws on
malformed input). These tests assert the shape of
+ * the produced {@link DataWeaveAst} for representative scripts, with emphasis
on operator precedence, postfix
+ * collection operations, header parsing, and graceful handling of unsupported
constructs. The AST records are exercised
+ * transitively here rather than in a separate test, since they carry no logic
of their own.
+ */
+class DataWeaveParserTest {
+
+ private static DataWeaveAst parseExpr(String src) {
+ var tokens = new DataWeaveLexer(src).tokenize();
+ return new DataWeaveParser(tokens).parseExpressionOnly();
+ }
+
+ private static DataWeaveAst parseScript(String src) {
+ var tokens = new DataWeaveLexer(src).tokenize();
+ return new DataWeaveParser(tokens).parse();
+ }
+
+ // ── Header ──
+
+ @Test
+ void shouldParseHeaderVersionAndOutputType() {
+ String dw = """
+ %dw 2.0
+ output application/json
+ ---
+ { greeting: "hello" }
+ """;
+ Script script = assertInstanceOf(Script.class, parseScript(dw));
+ assertEquals("2.0", script.header().version());
+ assertEquals("application/json", script.header().outputType());
+ assertTrue(script.header().inputs().isEmpty(), "no input declarations
expected");
+ ObjectLit body = assertInstanceOf(ObjectLit.class, script.body());
+ assertEquals(1, body.entries().size());
+ }
+
+ @Test
+ void shouldParseInputDeclarations() {
+ String dw = """
+ %dw 2.0
+ input payload application/xml
+ output application/json
+ ---
+ payload
+ """;
+ Script script = assertInstanceOf(Script.class, parseScript(dw));
+ assertEquals(1, script.header().inputs().size());
+ assertEquals("payload", script.header().inputs().get(0).name());
+ assertEquals("application/xml",
script.header().inputs().get(0).mediaType());
+ }
+
+ @Test
+ void shouldDefaultVersionWhenNoHeaderPresent() {
+ // an expression with no %dw header still yields a Script with the
default 2.0 version
+ Script script = assertInstanceOf(Script.class, parseScript("payload"));
+ assertEquals("2.0", script.header().version());
+ assertNull(script.header().outputType());
+ assertInstanceOf(Identifier.class, script.body());
+ }
+
+ // ── Literals and field access ──
+
+ @Test
+ void shouldParseLiterals() {
+ assertEquals("42", assertInstanceOf(NumberLit.class,
parseExpr("42")).value());
+ assertEquals("hi", assertInstanceOf(StringLit.class,
parseExpr("\"hi\"")).value());
+ assertTrue(assertInstanceOf(DataWeaveAst.BooleanLit.class,
parseExpr("true")).value());
+ assertInstanceOf(DataWeaveAst.NullLit.class, parseExpr("null"));
+ }
+
+ @Test
+ void shouldParseFieldAccessAndIndexAccessChain() {
+ // payload.items[0] -> IndexAccess(FieldAccess(Identifier(payload),
items), NumberLit(0))
+ IndexAccess idx = assertInstanceOf(IndexAccess.class,
parseExpr("payload.items[0]"));
+ assertEquals("0", assertInstanceOf(NumberLit.class,
idx.index()).value());
+ FieldAccess fa = assertInstanceOf(FieldAccess.class, idx.object());
+ assertEquals("items", fa.field());
+ assertEquals("payload", assertInstanceOf(Identifier.class,
fa.object()).name());
+ }
+
+ // ── Precedence ──
+
+ @Test
+ void shouldGiveMultiplicationHigherPrecedenceThanAddition() {
+ // 1 + 2 * 3 -> BinaryOp(+, 1, BinaryOp(*, 2, 3))
+ BinaryOp add = assertInstanceOf(BinaryOp.class, parseExpr("1 + 2 *
3"));
+ assertEquals("+", add.op());
+ assertEquals("1", assertInstanceOf(NumberLit.class,
add.left()).value());
+ BinaryOp mul = assertInstanceOf(BinaryOp.class, add.right());
+ assertEquals("*", mul.op());
+ assertEquals("2", assertInstanceOf(NumberLit.class,
mul.left()).value());
+ assertEquals("3", assertInstanceOf(NumberLit.class,
mul.right()).value());
+ }
+
+ @Test
+ void shouldGiveAndHigherPrecedenceThanOr() {
+ // a and b or c -> BinaryOp(or, BinaryOp(and, a, b), c)
+ BinaryOp or = assertInstanceOf(BinaryOp.class, parseExpr("a and b or
c"));
+ assertEquals("or", or.op());
+ assertEquals("c", assertInstanceOf(Identifier.class,
or.right()).name());
+ BinaryOp and = assertInstanceOf(BinaryOp.class, or.left());
+ assertEquals("and", and.op());
+ }
+
+ @Test
+ void shouldParseConcatOperator() {
+ BinaryOp op = assertInstanceOf(BinaryOp.class, parseExpr("\"a\" ++
\"b\""));
+ assertEquals("++", op.op());
+ }
+
+ @Test
+ void shouldParseParenthesizedGrouping() {
+ Parens parens = assertInstanceOf(Parens.class, parseExpr("(a + b)"));
+ assertEquals("+", assertInstanceOf(BinaryOp.class,
parens.expr()).op());
+ }
+
+ // ── Object / array literals ──
+
+ @Test
+ void shouldParseObjectLiteralWithStaticAndDynamicKeys() {
+ ObjectLit obj = assertInstanceOf(ObjectLit.class, parseExpr("{ name:
payload.x, (k): v }"));
+ assertEquals(2, obj.entries().size());
+ // static key
+ assertEquals("name", assertInstanceOf(Identifier.class,
obj.entries().get(0).key()).name());
+ assertFalse(obj.entries().get(0).dynamic());
+ // dynamic key parsed from a parenthesized expression
+ assertTrue(obj.entries().get(1).dynamic());
+ assertEquals("k", assertInstanceOf(Identifier.class,
obj.entries().get(1).key()).name());
+ }
+
+ @Test
+ void shouldParseArrayLiteral() {
+ ArrayLit arr = assertInstanceOf(ArrayLit.class, parseExpr("[1, 2,
3]"));
+ assertEquals(3, arr.elements().size());
+ assertEquals("2", assertInstanceOf(NumberLit.class,
arr.elements().get(1)).value());
+ }
+
+ // ── Collection operations / lambdas ──
+
+ @Test
+ void shouldParseMapWithExplicitLambda() {
+ MapExpr map = assertInstanceOf(MapExpr.class, parseExpr("payload map
((item) -> item.name)"));
+ assertEquals("payload", assertInstanceOf(Identifier.class,
map.collection()).name());
+ Lambda lambda = assertInstanceOf(Lambda.class, map.lambda());
+ assertEquals(1, lambda.params().size());
+ assertEquals("item", lambda.params().get(0).name());
+ assertInstanceOf(FieldAccess.class, lambda.body());
+ }
+
+ @Test
+ void shouldParseMapWithDollarShorthand() {
+ MapExpr map = assertInstanceOf(MapExpr.class, parseExpr("payload map
$.name"));
+ LambdaShorthand sh = assertInstanceOf(LambdaShorthand.class,
map.lambda());
+ assertEquals(List.of("name"), sh.fields());
+ }
+
+ // ── Function calls ──
+
+ @Test
+ void shouldParseBuiltinFunctionCall() {
+ FunctionCall call = assertInstanceOf(FunctionCall.class,
parseExpr("sizeOf(payload)"));
+ assertEquals("sizeOf", call.name());
+ assertEquals(1, call.args().size());
+ assertEquals("payload", assertInstanceOf(Identifier.class,
call.args().get(0)).name());
+ }
+
+ // ── Type coercion ──
+
+ @Test
+ void shouldParseTypeCoercionWithoutFormat() {
+ TypeCoercion tc = assertInstanceOf(TypeCoercion.class, parseExpr("x as
String"));
+ assertEquals("String", tc.type());
+ assertNull(tc.format());
+ }
+
+ @Test
+ void shouldParseTypeCoercionWithFormat() {
+ TypeCoercion tc = assertInstanceOf(TypeCoercion.class, parseExpr("x as
Date {format: \"yyyy-MM-dd\"}"));
+ assertEquals("Date", tc.type());
+ assertEquals("yyyy-MM-dd", tc.format());
+ }
+
+ // ── Control flow ──
+
+ @Test
+ void shouldParseIfElse() {
+ IfElse ie = assertInstanceOf(IfElse.class, parseExpr("if (a) b else
c"));
+ assertEquals("a", assertInstanceOf(Identifier.class,
ie.condition()).name());
+ assertEquals("b", assertInstanceOf(Identifier.class,
ie.thenExpr()).name());
+ assertEquals("c", assertInstanceOf(Identifier.class,
ie.elseExpr()).name());
+ }
+
+ @Test
+ void shouldParseDefaultExpression() {
+ DefaultExpr def = assertInstanceOf(DefaultExpr.class, parseExpr("a
default b"));
+ assertEquals("a", assertInstanceOf(Identifier.class,
def.expr()).name());
+ assertEquals("b", assertInstanceOf(Identifier.class,
def.fallback()).name());
+ }
+
+ @Test
+ void shouldParseVarDeclarationWithTrailingBody() {
+ // "var x = 1 x" -> VarDecl(x, NumberLit(1), body=Identifier(x))
+ VarDecl var = assertInstanceOf(VarDecl.class, parseExpr("var x = 1
x"));
+ assertEquals("x", var.name());
+ assertEquals("1", assertInstanceOf(NumberLit.class,
var.value()).value());
+ assertEquals("x", assertInstanceOf(Identifier.class,
var.body()).name());
+ }
+
+ // ── Unary ──
+
+ @Test
+ void shouldParseLogicalNot() {
+ UnaryOp not = assertInstanceOf(UnaryOp.class, parseExpr("not a"));
+ assertEquals("not", not.op());
+ assertEquals("a", assertInstanceOf(Identifier.class,
not.operand()).name());
+ }
+
+ // ── Graceful degradation ──
+
+ @Test
+ void shouldRepresentMatchExpressionAsUnsupported() {
+ Unsupported node = assertInstanceOf(Unsupported.class,
parseExpr("payload match { 1 }"));
+ assertEquals("match expression", node.reason());
+ assertTrue(node.originalText().startsWith("match"),
+ "original text should be retained, was: " +
node.originalText());
+ }
+
+ @Test
+ void shouldNotThrowOnUnbalancedInput() {
+ // expect() silently skips a missing RPAREN, so a best-effort AST is
still produced
+ List<Token> tokens = new DataWeaveLexer("(a + b").tokenize();
+ assertEquals(DataWeaveLexer.TokenType.EOF, tokens.get(tokens.size() -
1).type());
+ assertInstanceOf(Parens.class, parseExpr("(a + b"));
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/version/VersionSetTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/version/VersionSetTest.java
new file mode 100644
index 000000000000..23fc0f5a171f
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/version/VersionSetTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.
+ */
+package org.apache.camel.dsl.jbang.core.commands.version;
+
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.commands.UserConfigHelper;
+import org.apache.camel.dsl.jbang.core.commands.config.BaseConfigTestSupport;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.RuntimeType;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for {@link VersionSet}.
+ *
+ * The command persists the selected Camel version, repositories and runtime
into the user configuration property file.
+ * These tests pin down which keys are written (only the non-null ones), the
{@code --reset} behavior, and the choice
+ * between the global and local configuration file. Config file cleanup is
inherited from {@link BaseConfigTestSupport}.
+ */
+class VersionSetTest extends BaseConfigTestSupport {
+
+ @Test
+ void shouldStoreVersionInGlobalConfig() throws Exception {
+ UserConfigHelper.createUserConfig("");
+
+ VersionSet command = new VersionSet(new
CamelJBangMain().withPrinter(printer));
+ command.version = "4.10.0";
+ command.doCall();
+
+ CommandLineHelper.loadProperties(properties -> {
+ assertEquals("4.10.0", properties.get("camel-version"));
+ // repos and runtime were not requested, so they must not be
written
+ assertFalse(properties.containsKey("repos"), "repos must not be
set when no --repo is given");
+ assertFalse(properties.containsKey("runtime"), "runtime must not
be set when no --runtime is given");
+ });
+ }
+
+ @Test
+ void shouldStoreVersionRepoAndRuntimeTogether() throws Exception {
+ UserConfigHelper.createUserConfig("");
+
+ VersionSet command = new VersionSet(new
CamelJBangMain().withPrinter(printer));
+ command.version = "4.10.0";
+ command.repo = "https://repo.example.org/maven";
+ command.runtime = RuntimeType.springBoot;
+ command.doCall();
+
+ CommandLineHelper.loadProperties(properties -> {
+ assertEquals("4.10.0", properties.get("camel-version"));
+ assertEquals("https://repo.example.org/maven",
properties.get("repos"));
+ // runtime is stored using its canonical runtime() name, not the
enum constant
+ assertEquals(RuntimeType.springBoot.runtime(),
properties.get("runtime"));
+ });
+ }
+
+ @Test
+ void shouldLeaveExistingConfigUntouchedWhenNothingProvided() throws
Exception {
+ UserConfigHelper.createUserConfig("""
+ camel-version=4.9.0
+ """);
+
+ VersionSet command = new VersionSet(new
CamelJBangMain().withPrinter(printer));
+ // no version, repo, runtime or reset: doCall must not change the
stored value
+ command.doCall();
+
+ CommandLineHelper.loadProperties(properties -> assertEquals("4.9.0",
properties.get("camel-version")));
+ }
+
+ @Test
+ void shouldRemoveAllSettingsOnReset() throws Exception {
+ UserConfigHelper.createUserConfig("""
+ camel-version=4.9.0
+ repos=https://repo.example.org/maven
+ runtime=spring-boot
+ other=keep-me
+ """);
+
+ VersionSet command = new VersionSet(new
CamelJBangMain().withPrinter(printer));
+ command.reset = true;
+ command.doCall();
+
+ CommandLineHelper.loadProperties(properties -> {
+ assertFalse(properties.containsKey("camel-version"),
"camel-version must be removed on reset");
+ assertFalse(properties.containsKey("repos"), "repos must be
removed on reset");
+ assertFalse(properties.containsKey("runtime"), "runtime must be
removed on reset");
+ // unrelated settings are preserved: reset only clears the three
version keys
+ assertEquals("keep-me", properties.get("other"));
+ });
+ }
+
+ @Test
+ void shouldStoreVersionInLocalConfigWhenNotGlobal() throws Exception {
+ VersionSet command = new VersionSet(new
CamelJBangMain().withPrinter(printer));
+ command.version = "4.10.0";
+ command.global = false;
+ command.doCall();
+
+ // the value lands in the local config file, not the global one
+
assertTrue(Files.exists(Paths.get(CommandLineHelper.LOCAL_USER_CONFIG)),
+ "local config file should have been created");
+ CommandLineHelper.loadProperties(properties -> assertEquals("4.10.0",
properties.get("camel-version")), true);
+ }
+
+}