fix: complete shadow v3 operations safeguards
This commit is contained in:
@@ -1,18 +1,25 @@
|
||||
package com.ruoyi.algorithm.shadow;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ShadowV3OperationsPackageTest {
|
||||
private final Path root = projectRoot();
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void packageContainsAllCmdOnlyOperationsFiles() {
|
||||
assertThat(root.resolve("scripts/shadow-v3-preflight.cmd")).isRegularFile();
|
||||
@@ -37,6 +44,11 @@ class ShadowV3OperationsPackageTest {
|
||||
"jps -lv",
|
||||
"sqlcmd",
|
||||
"shadow_snapshot_state",
|
||||
"snapshot_generation>0",
|
||||
"snapshot_status='PUBLISHED'",
|
||||
"DATEDIFF(SECOND,synchronized_at,SYSUTCDATETIME()) BETWEEN 0 AND 300",
|
||||
"alg.shadow_instance_heartbeat",
|
||||
"DATEDIFF(SECOND,last_seen_at,SYSUTCDATETIME()) BETWEEN 0 AND 120",
|
||||
"FT546WMS_SHADOW_ONLY",
|
||||
"2026-09-03-3",
|
||||
"exit /b 1");
|
||||
@@ -78,7 +90,10 @@ class ShadowV3OperationsPackageTest {
|
||||
"shadow-v3-export.sql",
|
||||
"sqlcmd", "-E", "-b", "-r1",
|
||||
"FORTUCKY_SHADOW_",
|
||||
"shadow_failure");
|
||||
"shadow_failure",
|
||||
"OUTPUT_DIR already exists and is not empty",
|
||||
"environment-variable-names.txt",
|
||||
"if errorlevel 2");
|
||||
assertThat(exportSql).contains(
|
||||
"$(warehouseid)",
|
||||
"$(startutc)",
|
||||
@@ -128,6 +143,108 @@ class ShadowV3OperationsPackageTest {
|
||||
"EXPORT_OK");
|
||||
}
|
||||
|
||||
@Test
|
||||
void cmdScriptsHandleQuotedPathsAndPropagateToolFailures() throws Exception {
|
||||
Assumptions.assumeTrue(isWindows(), "CMD behavior is verified on Windows");
|
||||
Path fixture = Files.createDirectories(tempDir.resolve("fixture with spaces"));
|
||||
Path repo = fixture.resolve("repo with spaces");
|
||||
cloneRepository(repo);
|
||||
Path logDir = Files.createDirectories(fixture.resolve("logs with spaces"));
|
||||
Files.writeString(logDir.resolve("application log.txt"), "normal log line\n", StandardCharsets.UTF_8);
|
||||
Path stubBin = writeSqlcmdFailureStub(fixture);
|
||||
|
||||
CmdResult preflight = runCmd(root.resolve("scripts/shadow-v3-preflight.cmd"), stubBin,
|
||||
repo.toString(), gitHead(repo),
|
||||
"SQL SERVER WITH SPACE", "FT546 WMS");
|
||||
assertThat(preflight.exitCode()).isEqualTo(1);
|
||||
assertThat(preflight.output()).contains("SQL connectivity or shadow package validation failed");
|
||||
|
||||
Path output = fixture.resolve("new export with spaces");
|
||||
CmdResult export = runCmd(root.resolve("scripts/shadow-v3-export.cmd"), stubBin,
|
||||
repo.toString(), "SQL SERVER WITH SPACE", "FT546 WMS",
|
||||
"2026-09-03T00:00:00Z", output.toString(), logDir.toString());
|
||||
assertThat(export.exitCode()).isEqualTo(1);
|
||||
assertThat(export.output()).contains("read-only SQL export failed");
|
||||
}
|
||||
|
||||
@Test
|
||||
void exportRefusesToOverwriteExistingEvidence() throws Exception {
|
||||
Assumptions.assumeTrue(isWindows(), "CMD behavior is verified on Windows");
|
||||
Path fixture = Files.createDirectories(tempDir.resolve("repeat export fixture"));
|
||||
Path repo = Files.createDirectories(fixture.resolve("repo"));
|
||||
Files.createDirectory(repo.resolve(".git"));
|
||||
Path logDir = Files.createDirectories(fixture.resolve("logs"));
|
||||
Files.writeString(logDir.resolve("application.log"), "normal log line\n", StandardCharsets.UTF_8);
|
||||
Path output = Files.createDirectories(fixture.resolve("existing evidence"));
|
||||
Path marker = output.resolve("do-not-overwrite.txt");
|
||||
Files.writeString(marker, "preserve me", StandardCharsets.UTF_8);
|
||||
Path stubBin = writeSqlcmdFailureStub(fixture);
|
||||
|
||||
CmdResult result = runCmd(root.resolve("scripts/shadow-v3-export.cmd"), stubBin,
|
||||
repo.toString(), "server", "database", "2026-09-03T00:00:00Z",
|
||||
output.toString(), logDir.toString());
|
||||
|
||||
assertThat(result.exitCode()).isEqualTo(1);
|
||||
assertThat(result.output()).contains("OUTPUT_DIR already exists and is not empty");
|
||||
assertThat(Files.readString(marker, StandardCharsets.UTF_8)).isEqualTo("preserve me");
|
||||
assertThat(output.resolve("deployment.txt")).doesNotExist();
|
||||
}
|
||||
|
||||
private Path writeSqlcmdFailureStub(Path fixture) throws IOException {
|
||||
Path bin = Files.createDirectories(fixture.resolve("stub tools"));
|
||||
Path whereExecutable = Path.of(System.getenv("SystemRoot"), "System32", "where.exe");
|
||||
Files.copy(whereExecutable, bin.resolve("sqlcmd.exe"));
|
||||
return bin;
|
||||
}
|
||||
|
||||
private void cloneRepository(Path destination) throws Exception {
|
||||
CmdResult result = runProcess(new ProcessBuilder(
|
||||
"git", "clone", "--quiet", root.toString(), destination.toString()), 60);
|
||||
assertThat(result.exitCode()).as(result.output()).isZero();
|
||||
}
|
||||
|
||||
private String gitHead(Path repository) throws Exception {
|
||||
CmdResult result = runProcess(new ProcessBuilder(
|
||||
"git", "-C", repository.toString(), "rev-parse", "HEAD"), 15);
|
||||
assertThat(result.exitCode()).as(result.output()).isZero();
|
||||
return result.output().trim();
|
||||
}
|
||||
|
||||
private CmdResult runCmd(Path script, Path stubBin, String... arguments) throws Exception {
|
||||
StringBuilder command = new StringBuilder("call ").append(quote(script.toString()));
|
||||
for (String argument : arguments) {
|
||||
command.append(' ').append(quote(argument));
|
||||
}
|
||||
ProcessBuilder builder = new ProcessBuilder("cmd.exe", "/d", "/c", command.toString());
|
||||
String inheritedPath = builder.environment().getOrDefault("PATH", "");
|
||||
builder.environment().put("PATH", stubBin + File.pathSeparator + inheritedPath);
|
||||
builder.environment().put("FORTUCKY_ALGORITHM_WAREHOUSE_ID", "01");
|
||||
builder.redirectErrorStream(true);
|
||||
return runProcess(builder, 15);
|
||||
}
|
||||
|
||||
private CmdResult runProcess(ProcessBuilder builder, int timeoutSeconds) throws Exception {
|
||||
builder.redirectErrorStream(true);
|
||||
Process process = builder.start();
|
||||
boolean finished = process.waitFor(timeoutSeconds, TimeUnit.SECONDS);
|
||||
if (!finished) {
|
||||
process.destroyForcibly();
|
||||
}
|
||||
assertThat(finished).as("CMD script completed").isTrue();
|
||||
String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||
return new CmdResult(process.exitValue(), output);
|
||||
}
|
||||
|
||||
private static String quote(String value) {
|
||||
return "\"" + value.replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
|
||||
private static boolean isWindows() {
|
||||
return System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("windows");
|
||||
}
|
||||
|
||||
private record CmdResult(int exitCode, String output) { }
|
||||
|
||||
private String text(String relativePath) throws IOException {
|
||||
return Files.readString(root.resolve(relativePath), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user