Copilot commented on code in PR #5323:
URL: https://github.com/apache/solr/pull/5323#discussion_r4549256268


##########
solr/packaging/src/test/org/apache/solr/shell/BasicAuthTest.java:
##########
@@ -0,0 +1,148 @@
+/*
+ * 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.solr.shell;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.apache.lucene.util.IOUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Port of {@code test_basic_auth.bats}.
+ *
+ * <p>Kept close to the original, including the order of the tests and the 
text each asserts on.
+ * Where it differs:
+ *
+ * <ul>
+ *   <li>The original's {@code setup()} ran before every test; the harness 
wipes {@code SOLR_HOME}
+ *       once per class, so {@link #startSolrWithBasicAuth} does the rest. 
Each test gets its own
+ *       node, embedded ZooKeeper data and {@code security.json}, which is 
what lets two tests share
+ *       the collection name {@code COLL_NAME}.
+ *   <li>The one-command {@code 
SOLR_INCLUDE=/force/credentials/to/be/supplied} prefix becomes an
+ *       {@code env()} that stays set for the test, the harness environment 
being per-session rather
+ *       than per-command, and points inside the work directory rather than at 
an absolute Unix
+ *       path. That also keeps {@code solr auth disable} away from the 
distribution's own include
+ *       file, which every test class shares.
+ *   <li>{@code curl -I} becomes {@link #http}: its status is the original's 
{@code "HTTP/1.1 200"},
+ *       and the header assertion is lower-cased because {@code java.net.http} 
lower-cases header
+ *       names.
+ *   <li>The second test's credential-less {@code run solr create -c 
COLL_NAME}, which the original
+ *       leaves unasserted, asserts that it is refused.
+ * </ul>
+ */
+public class BasicAuthTest extends SolrShellTestCase {
+
+  @Before
+  public void startSolrWithBasicAuth() throws IOException {
+    // BATS' common_clean_setup: every test in this file starts its own Solr 
from a clean SOLR_HOME.
+    IOUtils.rm(harnessEnv.solrHome());
+    Files.createDirectories(harnessEnv.logsDir());
+
+    run("solr start -Dsolr.packages.enabled=true");
+
+    // The auth command exports some system variables that are injected as 
basic auth username and
+    // password, however that defeats our test so fake that out by pointing 
SOLR_INCLUDE at a path
+    // that does not exist: 'auth enable' then writes neither the include file 
nor
+    // SOLR_HOME/basicAuth.conf, and every command below has to supply the 
credentials itself.
+    Path bogusInclude = 
workDir().resolve("force-credentials-to-be-supplied").resolve("solr.in.sh");
+    env("SOLR_INCLUDE", bogusInclude.toString());
+    run("solr auth enable --type basicAuth --credentials name:password");
+
+    run(
+        "solr assert --credentials name:password --cloud 
http://localhost:${SOLR_PORT}";
+            + " --timeout 5000");
+  }
+
+  @After
+  public void disableBasicAuth() {
+    // BATS' teardown. Stopping the node is automatic; SOLR_INCLUDE still 
points at the path that
+    // does not exist, so this only clears security.json out of ZooKeeper.
+    run("solr auth disable -z localhost:${ZK_PORT}");
+  }
+
+  // Remaining commands that should support basic auth:
+  // package, export
+
+  @Test
+  public void createConfigApiAndDeleteWithBasicAuth() {
+    // Test create
+    run("solr create -u name:password -c COLL_NAME")
+        .assertContains("Created collection 'COLL_NAME'");
+
+    // Test config
+    run("solr config -u name:password -c COLL_NAME --action set-property 
--property"
+            + " updateHandler.autoCommit.maxDocs --value 100 --solr-url"
+            + " http://localhost:${SOLR_PORT}/solr";)
+        .assertContains("Successfully set-property 
updateHandler.autoCommit.maxDocs to 100");
+
+    // Test api
+    run("solr api -u name:password --solr-url"
+            + " \"http://localhost:${SOLR_PORT}/solr/COLL_NAME/select?q=*:*\"; 
--verbose")
+        .assertContains("\"numFound\":0");
+
+    // Test delete
+    run("solr delete --credentials name:password -c COLL_NAME -z 
localhost:${ZK_PORT}"
+            + " --delete-config --verbose")
+        .assertContains("Deleted collection 'COLL_NAME'");
+    assertFalse(collectionExists("COLL_NAME"));
+    assertFalse(configExists("COLL_NAME"));
+  }
+
+  @Test
+  public void postPostlogsAndExportWithBasicAuth() {
+    // Strengthened: the original ignores this line's outcome, but without 
credentials the create
+    // has to be refused.
+    runFail("solr create -c COLL_NAME");
+    run("solr create -u name:password -c COLL_NAME")
+        .assertContains("Created collection 'COLL_NAME'");
+
+    // Test post
+    // --name COLL_NAME, not the BATS original's 'monitors': no test ever 
created that collection,
+    // so every document 404'd. It passed anyway because the status was 
ignored and `solr post`
+    // prints "1 files indexed." even when the collection does not exist 
(recorded separately).
+    run("solr post -u name:password -t application/xml --solr-url"
+            + " http://localhost:${SOLR_PORT} --name COLL_NAME"
+            + " ${SOLR_TIP}/example/exampledocs/monitor.xml")
+        .assertContains("1 files indexed.");
+
+    // Test postlogs
+    run("solr postlogs -u name:password --solr-url 
http://localhost:${SOLR_PORT}";
+            + " --name COLL_NAME --rootdir ${SOLR_LOGS_DIR}/solr.log")

Review Comment:
   The harness substitutes these paths before shell parsing, so spaces in 
`SOLR_TIP` or `SOLR_LOGS_DIR` split them into multiple arguments and make this 
authentication test fail for an unrelated path constraint. Quote both the file 
argument here and the `--rootdir` value at line 128, matching `PostTest` and 
`PostlogsTest`.



##########
solr/packaging/src/test/org/apache/solr/shell/SecurityManagerTest.java:
##########
@@ -0,0 +1,85 @@
+/*
+ * 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.solr.shell;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import org.junit.Test;
+
+/**
+ * Port of {@code test_security_manager.bats}.
+ *
+ * <p>Kept close to the original, including the order of the tests and the 
text each asserts on.
+ * Where it differs:
+ *
+ * <ul>
+ *   <li>the two directories are created with {@link #mkdirs} and 
canonicalized with {@code
+ *       toRealPath()}, instead of {@code mkdir -p} followed by {@code cd -P 
&& pwd};
+ *   <li>the backup {@code location} is built from {@link Path#toUri()} rather 
than by gluing {@code
+ *       file://} onto the path — the same string as the original produces on 
Unix, and a valid file
+ *       URL on Windows too, where a bare {@code file://C:\...} would not be;
+ *   <li>{@code solr start} and {@code solr create}, which the original ran 
without looking at the
+ *       outcome, assert their exit status;
+ *   <li>the rejected backup also asserts a non-zero exit status; the request 
is answered with HTTP
+ *       500 either way the rejection happens.
+ * </ul>
+ *
+ * <p>The node's security manager is what this suite is about. On Java 24 and 
later {@code bin/solr}
+ * does not install one, and Solr's own {@code allowPaths} check rejects the 
unpermitted directory
+ * instead — hence the two alternatives in the final assertion, exactly as in 
the original.
+ */
+public class SecurityManagerTest extends SolrShellTestCase {
+
+  @Test
+  public void allowPathsBackup() throws IOException {
+    // Make a test tmp dir, as the security policy includes TMP, so that might 
already contain the
+    // session work directory.
+    Path testTmpDir = mkdirs("tmp").toRealPath();
+
+    Path backupDir = mkdirs("backup-dir").toRealPath();
+
+    env("SOLR_SECURITY_MANAGER_ENABLED", "true");
+    env(
+        "SOLR_OPTS",
+        "-Dsolr.security.allow.paths=" + backupDir + " -Djava.io.tmpdir=" + 
testTmpDir);

Review Comment:
   When either directory contains spaces, `bin/solr` splits these unquoted 
`SOLR_OPTS` values into extra JVM arguments and Solr fails to start. Pass the 
properties through an argument-preserving startup path for each shell; on bash, 
individually quoted command-line `-D` arguments avoid this split, whereas 
quotes embedded in `SOLR_OPTS` do not.



##########
solr/packaging/src/test/org/apache/solr/shell/ZkTest.java:
##########
@@ -0,0 +1,210 @@
+/*
+ * 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.solr.shell;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+import org.apache.lucene.util.IOUtils;
+import org.apache.solr.shell.exec.Shell;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Port of {@code test_zk.bats}.
+ *
+ * <p>Kept deliberately close to the original so the two can be compared line 
by line, including the
+ * order of the tests and the text each one asserts on. Four things differ, 
each for a reason the
+ * comment at the site explains: files are created through Java rather than 
{@code touch}, {@code
+ * curl} becomes {@link #http}, the symlink test works on a copy of the config 
set instead of
+ * mutating the shared distribution, and the {@code ZK_HOST} test asserts only 
the fallback message,
+ * not the original's refutation of a listing from port 8983.
+ */
+public class ZkTest extends SolrShellTestCase {
+
+  @BeforeClass
+  public static void startSolr() {
+    // BATS' setup_file. The node lives for the whole class and is stopped 
automatically.
+    classCli.run("solr start -DminStateByteLenForCompression=0");
+  }
+
+  @Test
+  public void shortHelp() {
+    run("solr zk ls -h").assertContains("usage: bin/solr zk");
+  }
+
+  @Test
+  public void shortHelpIsInferred() {
+    runFail("solr zk ls").assertContains("usage: bin/solr zk");
+  }
+
+  @Test
+  public void longHelp() {
+    run("solr zk -h")
+        .assertContains("Pass --help or -h after any COMMAND")
+        .assertContains("updateacls");
+  }
+
+  @Test
+  public void listingOutFiles() {
+    run("solr zk ls / -z localhost:${ZK_PORT} 
--recursive").assertContains("aliases.json");
+  }
+
+  @Test
+  public void connectingViaVariousSolrUrlsAndZkHosts() {
+    run("solr zk ls / --solr-url 
http://localhost:${SOLR_PORT}";).assertContains("aliases.json");
+    run("solr zk ls / --solr-connection http://localhost:${SOLR_PORT}";)
+        .assertContains("aliases.json");
+    run("solr zk ls / -s 
http://localhost:${SOLR_PORT}";).assertContains("aliases.json");
+    run("solr zk ls /").assertContains("aliases.json");
+    run("solr zk ls / -z localhost:${ZK_PORT}").assertContains("aliases.json");
+    run("solr zk ls / --zk-host 
localhost:${ZK_PORT}").assertContains("aliases.json");
+  }
+
+  @Test
+  public void copyingFilesAround() {
+    // touch() rather than the shell's, which cmd.exe does not have. No cd() 
needed: a session
+    // starts in its own work directory, so these land there and so does the 
download below.
+    touch("myfile.txt");
+    touch("myfile2.txt");
+    touch("myfile3.txt");
+
+    run("solr zk cp myfile.txt zk:/myfile.txt -z localhost:${ZK_PORT}")
+        .assertContains(
+            "Copying from 'myfile.txt' to 'zk:/myfile.txt'. ZooKeeper at 
localhost:${ZK_PORT}");
+    run("solr zk ls / -z localhost:${ZK_PORT}").assertContains("myfile.txt");
+
+    run("solr zk cp myfile2.txt zk:myfile2.txt -z localhost:${ZK_PORT}");
+    run("solr zk ls / -z localhost:${ZK_PORT}").assertContains("myfile2.txt");
+
+    run("solr zk cp myfile3.txt zk:/myfile3.txt -z localhost:${ZK_PORT}")
+        .assertContains(
+            "Copying from 'myfile3.txt' to 'zk:/myfile3.txt'. ZooKeeper at 
localhost:${ZK_PORT}");
+    run("solr zk ls / -z localhost:${ZK_PORT}").assertContains("myfile3.txt");
+
+    run(
+        """
+        solr zk cp zk:/ -r "recursive_download/"
+        """);
+    Path download = workDir().resolve("recursive_download");
+    assertTrue(Files.exists(download.resolve("myfile.txt")));
+    assertTrue(Files.exists(download.resolve("myfile2.txt")));
+    assertTrue(Files.exists(download.resolve("myfile3.txt")));
+  }
+
+  @Test
+  public void upconfig() {
+    run("""
+        solr zk upconfig -d 
"${SOLR_TIP}/server/solr/configsets/sample_techproducts_configs" \
+            -n techproducts2 -z localhost:${ZK_PORT}
+        """)
+        .assertContains("Uploading")
+        .assertNotContains("ERROR");
+
+    // BATS polled with curl and grep; the harness' own HTTP helper needs no 
curl, which is what
+    // makes this work on Windows.
+    awaitOrFail(
+        "config set techproducts2 to appear",
+        Duration.ofSeconds(10),
+        Duration.ofSeconds(1),
+        () -> http("/api/configsets").body().contains("techproducts2"));
+    assertTrue(
+        
http("/api/configsets").body().contains("\"configSets\":[\"_default\",\"techproducts2\"]"));
+  }
+
+  @Test
+  @EnabledOnShell(
+      Shell.BASH) // Creating a symlink on Windows needs privileges a test 
cannot assume.
+  public void solr12429UpconfigFailsWithSymlink() throws IOException {
+    // The BATS original symlinked inside the distribution's own config set 
and deleted the links
+    // afterwards. Here it works on a copy: the distribution is shared by 
every test class, so
+    // mutating it would make this test unsafe to run alongside any other.
+    Path configSet = workDir().resolve("configset");
+    copyDirectory(
+        
harnessEnv.solrTip().resolve("server/solr/configsets/sample_techproducts_configs"),
+        configSet);
+
+    Path conf = configSet.resolve("conf");
+    Files.createSymbolicLink(
+        conf.resolve("symlinked_stopwords.txt"), 
conf.resolve("stopwords.txt"));
+    Files.createSymbolicLink(conf.resolve("language"), conf.resolve("lang"));
+    assertTrue(Files.isSymbolicLink(conf.resolve("symlinked_stopwords.txt")));
+    assertTrue(Files.isSymbolicLink(conf.resolve("language")));
+
+    runFail(
+            """
+        solr zk upconfig -d "configset" -n techproducts_with_symlinks -z 
localhost:${ZK_PORT}
+        """)
+        .assertContains("Uploading")
+        .assertContains("ERROR: Not uploading symbolic link");
+  }
+
+  @Test
+  public void downconfig() {
+    run("""
+        solr zk downconfig -z localhost:${ZK_PORT} -n _default -d "downconfig"
+        """)
+        .assertContains("Downloading")
+        .assertNotContains("ERROR");
+  }
+
+  @Test
+  public void zkCpGetsSolrHomeFromTheCommandLineOption() {
+    touch("afile.txt");
+
+    run("solr zk cp afile.txt zk:/afile.txt -z localhost:${ZK_PORT} --verbose"
+            + " --solr-home ${SOLR_TIP}/server/solr")

Review Comment:
   The harness substitutes SOLR_TIP before invoking the shell but does not 
quote its value. With a checkout or distribution path containing spaces, 
--solr-home receives only the first path segment, so this test no longer 
exercises the intended home and its assertion fails. Quote the complete path 
argument, as the upconfig test above does.



##########
solr/packaging/src/test/org/apache/solr/shell/ZkTest.java:
##########
@@ -0,0 +1,210 @@
+/*
+ * 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.solr.shell;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+import org.apache.lucene.util.IOUtils;
+import org.apache.solr.shell.exec.Shell;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Port of {@code test_zk.bats}.
+ *
+ * <p>Kept deliberately close to the original so the two can be compared line 
by line, including the
+ * order of the tests and the text each one asserts on. Four things differ, 
each for a reason the
+ * comment at the site explains: files are created through Java rather than 
{@code touch}, {@code
+ * curl} becomes {@link #http}, the symlink test works on a copy of the config 
set instead of
+ * mutating the shared distribution, and the {@code ZK_HOST} test asserts only 
the fallback message,
+ * not the original's refutation of a listing from port 8983.
+ */
+public class ZkTest extends SolrShellTestCase {
+
+  @BeforeClass
+  public static void startSolr() {
+    // BATS' setup_file. The node lives for the whole class and is stopped 
automatically.
+    classCli.run("solr start -DminStateByteLenForCompression=0");
+  }
+
+  @Test
+  public void shortHelp() {
+    run("solr zk ls -h").assertContains("usage: bin/solr zk");
+  }
+
+  @Test
+  public void shortHelpIsInferred() {
+    runFail("solr zk ls").assertContains("usage: bin/solr zk");
+  }
+
+  @Test
+  public void longHelp() {
+    run("solr zk -h")
+        .assertContains("Pass --help or -h after any COMMAND")
+        .assertContains("updateacls");
+  }
+
+  @Test
+  public void listingOutFiles() {
+    run("solr zk ls / -z localhost:${ZK_PORT} 
--recursive").assertContains("aliases.json");
+  }
+
+  @Test
+  public void connectingViaVariousSolrUrlsAndZkHosts() {
+    run("solr zk ls / --solr-url 
http://localhost:${SOLR_PORT}";).assertContains("aliases.json");
+    run("solr zk ls / --solr-connection http://localhost:${SOLR_PORT}";)
+        .assertContains("aliases.json");
+    run("solr zk ls / -s 
http://localhost:${SOLR_PORT}";).assertContains("aliases.json");
+    run("solr zk ls /").assertContains("aliases.json");
+    run("solr zk ls / -z localhost:${ZK_PORT}").assertContains("aliases.json");
+    run("solr zk ls / --zk-host 
localhost:${ZK_PORT}").assertContains("aliases.json");
+  }
+
+  @Test
+  public void copyingFilesAround() {
+    // touch() rather than the shell's, which cmd.exe does not have. No cd() 
needed: a session
+    // starts in its own work directory, so these land there and so does the 
download below.
+    touch("myfile.txt");
+    touch("myfile2.txt");
+    touch("myfile3.txt");
+
+    run("solr zk cp myfile.txt zk:/myfile.txt -z localhost:${ZK_PORT}")
+        .assertContains(
+            "Copying from 'myfile.txt' to 'zk:/myfile.txt'. ZooKeeper at 
localhost:${ZK_PORT}");
+    run("solr zk ls / -z localhost:${ZK_PORT}").assertContains("myfile.txt");
+
+    run("solr zk cp myfile2.txt zk:myfile2.txt -z localhost:${ZK_PORT}");
+    run("solr zk ls / -z localhost:${ZK_PORT}").assertContains("myfile2.txt");
+
+    run("solr zk cp myfile3.txt zk:/myfile3.txt -z localhost:${ZK_PORT}")
+        .assertContains(
+            "Copying from 'myfile3.txt' to 'zk:/myfile3.txt'. ZooKeeper at 
localhost:${ZK_PORT}");
+    run("solr zk ls / -z localhost:${ZK_PORT}").assertContains("myfile3.txt");
+
+    run(
+        """
+        solr zk cp zk:/ -r "recursive_download/"
+        """);
+    Path download = workDir().resolve("recursive_download");
+    assertTrue(Files.exists(download.resolve("myfile.txt")));
+    assertTrue(Files.exists(download.resolve("myfile2.txt")));
+    assertTrue(Files.exists(download.resolve("myfile3.txt")));
+  }
+
+  @Test
+  public void upconfig() {
+    run("""
+        solr zk upconfig -d 
"${SOLR_TIP}/server/solr/configsets/sample_techproducts_configs" \
+            -n techproducts2 -z localhost:${ZK_PORT}
+        """)
+        .assertContains("Uploading")
+        .assertNotContains("ERROR");
+
+    // BATS polled with curl and grep; the harness' own HTTP helper needs no 
curl, which is what
+    // makes this work on Windows.
+    awaitOrFail(
+        "config set techproducts2 to appear",
+        Duration.ofSeconds(10),
+        Duration.ofSeconds(1),
+        () -> http("/api/configsets").body().contains("techproducts2"));
+    assertTrue(
+        
http("/api/configsets").body().contains("\"configSets\":[\"_default\",\"techproducts2\"]"));
+  }
+
+  @Test
+  @EnabledOnShell(
+      Shell.BASH) // Creating a symlink on Windows needs privileges a test 
cannot assume.
+  public void solr12429UpconfigFailsWithSymlink() throws IOException {
+    // The BATS original symlinked inside the distribution's own config set 
and deleted the links
+    // afterwards. Here it works on a copy: the distribution is shared by 
every test class, so
+    // mutating it would make this test unsafe to run alongside any other.
+    Path configSet = workDir().resolve("configset");
+    copyDirectory(
+        
harnessEnv.solrTip().resolve("server/solr/configsets/sample_techproducts_configs"),
+        configSet);
+
+    Path conf = configSet.resolve("conf");
+    Files.createSymbolicLink(
+        conf.resolve("symlinked_stopwords.txt"), 
conf.resolve("stopwords.txt"));
+    Files.createSymbolicLink(conf.resolve("language"), conf.resolve("lang"));
+    assertTrue(Files.isSymbolicLink(conf.resolve("symlinked_stopwords.txt")));
+    assertTrue(Files.isSymbolicLink(conf.resolve("language")));
+
+    runFail(
+            """
+        solr zk upconfig -d "configset" -n techproducts_with_symlinks -z 
localhost:${ZK_PORT}
+        """)
+        .assertContains("Uploading")
+        .assertContains("ERROR: Not uploading symbolic link");
+  }
+
+  @Test
+  public void downconfig() {
+    run("""
+        solr zk downconfig -z localhost:${ZK_PORT} -n _default -d "downconfig"
+        """)
+        .assertContains("Downloading")
+        .assertNotContains("ERROR");
+  }
+
+  @Test
+  public void zkCpGetsSolrHomeFromTheCommandLineOption() {
+    touch("afile.txt");
+
+    run("solr zk cp afile.txt zk:/afile.txt -z localhost:${ZK_PORT} --verbose"
+            + " --solr-home ${SOLR_TIP}/server/solr")
+        .assertContains("Using SolrHome: ${SOLR_TIP}/server/solr")
+        .assertNotContains("Failed to load solr.xml from ZK or SolrHome");
+  }
+
+  @Test
+  public void envVarZkHostIsHonored() {
+    // SOLR_PORT_LISTEN has to be absent, not empty: the tool looks at whether 
it is set at all
+    // before falling back to the default port.
+    unset("SOLR_PORT_LISTEN");
+
+    // Only the fallback message is asserted, not that the listing came back 
empty. The BATS
+    // original also refuted "aliases.json", which assumes nothing is 
listening on 8983 — a port no
+    // test class owns, and the single most likely thing to be running on a 
Solr developer's own
+    // machine. That made the test fail for a reason having nothing to do with 
ZK_HOST.
+    runFail("solr zk ls / --recursive")

Review Comment:
   This still fails if a developer already has a reachable SolrCloud instance 
on port 8983. After `SOLR_PORT_LISTEN` is unset, the CLI can discover that 
instance's ZooKeeper and successfully list it, causing `runFail()` to throw 
before the warning assertion. Use `tryRun()` here to leave the status 
unconstrained, as the comment intends; the subsequent command can still assert 
success against this class's ZooKeeper.



##########
solr/test-framework/src/java/org/apache/solr/shell/exec/Shell.java:
##########
@@ -0,0 +1,142 @@
+/*
+ * 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.solr.shell.exec;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+/** The shells a command line can be handed to. One process per command; no 
interactive session. */
+public enum Shell {
+
+  /** POSIX shell. Invoked with a single {@code -c} argument, no profile and 
no rc files. */
+  BASH {
+    @Override
+    public List<String> processArgs(String line, Path scriptDir, String 
scriptName) {
+      return List.of("bash", "--noprofile", "--norc", "-c", line);
+    }
+
+    @Override
+    public String quote(String arg) {
+      return "'" + arg.replace("'", "'\\''") + "'";
+    }
+  },
+
+  /**
+   * Windows {@code cmd.exe}.
+   *
+   * <p>The line goes into a temp {@code .cmd} script rather than onto the 
{@code cmd /c} command
+   * line, because {@code ProcessBuilder}'s quoting rules for {@code cmd /c} 
and {@code .cmd}
+   * arguments are special and fragile. {@code call} is prepended to a 
single-command line — without
+   * it a batch file invoking {@code solr.cmd} never gets control back — while 
a multi-line body is
+   * written verbatim and is responsible for its own. {@code /d} disables 
AutoRun registry hooks.
+   */
+  CMD {
+    @Override
+    public List<String> processArgs(String line, Path scriptDir, String 
scriptName)
+        throws IOException {
+      String command = line.contains("\n") ? line : "call " + line;
+      String body =
+          String.join("\r\n", "@echo off", "chcp 65001 >nul", command, "exit 
/b %ERRORLEVEL%")
+              + "\r\n";
+      Path script = scriptDir.resolve(scriptName + ".cmd");
+      Files.writeString(
+          script, body.replace("\n", "\r\n").replace("\r\r\n", "\r\n"), 
StandardCharsets.UTF_8);
+      return List.of("cmd.exe", "/d", "/c", script.toString());
+    }
+
+    /**
+     * Double quotes protect {@code & | < > ( )} from cmd's parser, and {@code 
!} is literal because
+     * these scripts never enable delayed expansion. {@code %} is expanded 
even inside double
+     * quotes, so a literal one is written {@code %%} as any batch file must.
+     *
+     * <p>Two characters are refused outright, cmd offering no escape that is 
both correct and
+     * predictable: a double quote, which has no escape surviving cmd's quote 
counting and the
+     * target program's parser at once, and a caret, which the {@code call} 
used for a single
+     * command doubles as it re-parses its line, so {@code "a^b"} arrives as 
{@code "a^^b"}. Pass
+     * either with {@code script(bashBody, cmdBody)}.
+     */
+    @Override
+    public String quote(String arg) {
+      if (arg.indexOf('"') >= 0) {
+        throw new UnsupportedOperationException(
+            "cmd.exe has no escape for a double quote that survives both its 
own quote counting "
+                + "and the target program's parser. Use script(bashBody, 
cmdBody) for: "
+                + arg);
+      }
+      if (arg.indexOf('^') >= 0) {
+        throw new UnsupportedOperationException(
+            "cmd.exe doubles a caret when 'call' re-parses the line, so it 
cannot be passed as an "
+                + "argument. Use script(bashBody, cmdBody) for: "
+                + arg);
+      }
+      String escaped = arg.replace("%", "%%");
+      return NEEDS_CMD_QUOTING.matcher(arg).find() ? '"' + escaped + '"' : 
escaped;

Review Comment:
   An empty argument does not match NEEDS_CMD_QUOTING, so quote("") returns an 
empty string. Consequently, run(List.of("solr", "…", "")) drops that argument 
on cmd.exe instead of passing an empty value, while the bash implementation 
preserves it. Quote empty strings as well so argv-based tests exercise the same 
arguments on both platforms.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to