Files
agiledancers/ci/CiTests.java

184 lines
11 KiB
Java

import java.nio.file.*;
import java.time.Duration;
import java.util.*;
/** Dependency-free behavior tests: java ci/Ci.java test. */
class CiTests {
static final String SHA = "a".repeat(40);
static int count;
interface Test { void run() throws Exception; }
static void test(String name, Test test) throws Exception {
test.run(); count++; System.out.println("PASS " + name);
}
static void equal(Object expected, Object actual) {
if (!Objects.equals(expected,actual)) throw new AssertionError("Expected " + expected + ", got " + actual);
}
static void failure(Test action) throws Exception {
try { action.run(); } catch (Exception expected) { return; }
throw new AssertionError("Expected failure");
}
static void run() throws Exception {
count = 0;
test("JSON preserves configuration values and escaped text", () -> {
String json = "{\"text\":\"line\\nquote\\\"\\u20ac\",\"values\":[null,true,false,1,-2.5e2],\"nested\":{}}";
Object data = Json.parse(json); equal(data,Json.parse(Json.write(data)));
equal("line\nquote\"€",Json.object(data).get("text"));
});
test("JSON rejects malformed/ambiguous input", () -> {
for (String value : List.of("{\"a\":null,\"a\":1}","{} []","[1,]","01","\"\\uZZZZ\"","\"line\n\"","truefalse","+1"))
failure(() -> Json.parse(value));
});
test("Docker configuration merge is idempotent and preserves backup", () -> {
Path dir = Files.createTempDirectory("ci-docker-test-");
try {
Path file = dir.resolve("daemon.json");
String original = "{\"features\":{\"buildkit\":true},\"insecure-registries\":[\"existing:5000\"]}";
Files.writeString(file,original);
Setup.configureDocker(file); String first = Files.readString(file); Setup.configureDocker(file);
equal(first,Files.readString(file));
equal(List.of("existing:5000","192.168.2.28:3000"),Json.object(Json.parse(first)).get("insecure-registries"));
equal(original,Files.readString(dir.resolve("daemon.json.before-agiledancers")));
} finally { Pipeline.deleteTree(dir); }
});
test("Invalid Docker configuration is unchanged", () -> {
Path dir = Files.createTempDirectory("ci-docker-test-");
try {
Path file = dir.resolve("daemon.json"); String original = "{\"insecure-registries\":\"invalid\"}";
Files.writeString(file,original); failure(() -> Setup.configureDocker(file)); equal(original,Files.readString(file));
} finally { Pipeline.deleteTree(dir); }
});
test("Feature branches never contact registry", () -> {
try (Fixture f = new Fixture()) {
f.pipeline.env.put("CI_REF","refs/heads/feature/test"); f.pipeline.publish(); equal(0L,f.calls("login")); equal(0L,f.calls("push"));
}
});
test("Unverified commit cannot publish", () -> {
try (Fixture f = new Fixture()) {
Files.delete(f.root.resolve("ci/artifacts/verified-commit")); failure(f.pipeline::publish); equal(0L,f.calls("push"));
}
});
test("Publication pullback and rerun preserve commit tags", () -> {
try (Fixture f = new Fixture()) {
f.pipeline.publish(); equal(4L,f.calls("push")); f.pipeline.publish(); equal(6L,f.calls("push")); equal(8L,f.calls("pull"));
}
});
test("Partial publication is completed without overwriting backend", () -> {
try (Fixture f = new Fixture()) {
f.fake.pushFailure = "frontend:" + SHA; failure(f.pipeline::publish);
equal(0L,f.fake.calls.stream().filter(a -> a.contains("push") && a.getLast().endsWith(":main")).count());
f.fake.pushFailure = ""; f.pipeline.publish();
equal(1L,f.fake.calls.stream().filter(a -> a.contains("push") && a.getLast().endsWith("backend:" + SHA)).count());
}
});
test("Authentication error is not a missing tag", () -> {
try (Fixture f = new Fixture()) { f.fake.manifestError = true; failure(f.pipeline::publish); equal(0L,f.calls("push")); }
});
test("Pull failure blocks moving tags", () -> {
try (Fixture f = new Fixture()) {
f.fake.pullFailure = "backend:" + SHA; failure(f.pipeline::publish);
equal(0L,f.fake.calls.stream().filter(a -> a.contains("push") && a.getLast().endsWith(":main")).count());
}
});
test("Older commit cannot move main", () -> {
try (Fixture f = new Fixture()) { f.fake.head = "c".repeat(40); f.pipeline.publish(); equal(2L,f.calls("push")); }
});
test("Unavailable Docker fails before build with a concrete cause", () -> {
try (Fixture f = new Fixture()) {
f.fake.dockerAvailable = false;
try { f.pipeline.verify(); throw new AssertionError("Expected failure"); }
catch (IllegalArgumentException error) { equal(true,error.getMessage().contains("Docker is unavailable")); }
equal(0L,f.fake.calls.stream().filter(a -> a.getFirst().equals("mvn")).count());
}
});
test("Architecture mismatch blocks publication", () -> {
try (Fixture f = new Fixture()) { f.pipeline.env.put("TARGET_PLATFORM","linux/arm64"); failure(f.pipeline::publish); equal(0L,f.calls("push")); }
});
test("Failed verification removes success marker and preserves exit status", () -> {
try (Fixture f = new Fixture()) {
f.fake.mavenFailure = true;
try { f.pipeline.verify(); throw new AssertionError("Expected failure"); }
catch (Commands.Failure error) { equal(42,error.exitCode); }
equal(List.of("mvn", "-B", "-ntp", "-Dformat.mode=check", "clean", "verify"),
f.fake.calls.stream().filter(a -> a.getFirst().equals("mvn")).findFirst().orElseThrow());
equal(false,Files.exists(f.root.resolve("ci/artifacts/verified-commit")));
equal(0L,f.calls("build")); equal(0L,f.calls("push"));
}
});
test("Frontend lint runs after install and blocks tests and build on failure", () -> {
try (Fixture f = new Fixture()) {
f.fake.lintFailure = true;
try { f.pipeline.verifyFrontend(); throw new AssertionError("Expected failure"); }
catch (Commands.Failure error) { equal(1,error.exitCode); }
equal(List.of(List.of("npm", "ci"), List.of("npm", "run", "lint")), f.fake.calls);
}
});
test("Frontend verification checks before testing and building", () -> {
try (Fixture f = new Fixture()) {
f.pipeline.verifyFrontend();
equal(List.of("ci", "run lint", "test -- --reporters=default --reporters=junit --output-file=unit-results/junit.xml", "run build"),
f.fake.calls.stream().map(a -> String.join(" ", a.subList(1,a.size()))).toList());
}
});
test("Process execution enforces its deadline", () -> {
Path dir = Files.createTempDirectory("ci-process-test-");
try {
Path file = dir.resolve("Sleep.java");
Files.writeString(file,"class Sleep { public static void main(String[] args) throws Exception { Thread.sleep(10000); } }");
Commands runner = new Commands(Duration.ofMillis(100));
try {
runner.execute(List.of(System.getProperty("java.home") + "/bin/java",file.toString()),dir,System.getenv(),null,false);
throw new AssertionError("Expected timeout");
} catch (Commands.Failure error) { equal(124,error.exitCode); }
} finally { Pipeline.deleteTree(dir); }
});
System.out.println(count + " CI tests passed");
}
static class Fixture implements AutoCloseable {
final Path root = Files.createTempDirectory("ci-publication-test-");
final FakeCommands fake = new FakeCommands();
final Pipeline pipeline;
Fixture() throws Exception {
Map<String,String> env = new HashMap<>(System.getenv());
env.putAll(Map.of("CI_COMMIT",SHA,"CI_RUN_ID","test","CI_RUN_ATTEMPT","1","CI_REF","refs/heads/main",
"CI_REPOSITORY","wanjakrah/agiledancers","TARGET_PLATFORM","linux/amd64","GITEA_SERVER_URL","http://192.168.2.28:3000",
"REGISTRY_USER","test","REGISTRY_TOKEN","dummy-token","CI_PUBLISH_LOCK_DIR",root.resolve("lock").toString()));
pipeline = new Pipeline(root,env,fake) { @Override String mainHead() { return fake.head; } };
Files.writeString(root.resolve("ci/artifacts/verified-commit"),SHA + "\n");
}
long calls(String verb) { return fake.calls.stream().filter(a -> a.size()>1 && a.getFirst().equals("docker") && a.get(1).equals(verb)).count(); }
public void close() throws Exception { pipeline.closeDockerConfig(); Pipeline.deleteTree(root); }
}
static class FakeCommands extends Commands {
final List<List<String>> calls = new ArrayList<>();
final Map<String,String> images = new HashMap<>(), tags = new HashMap<>();
String head = SHA, pushFailure = "", pullFailure = "";
boolean manifestError, mavenFailure, lintFailure;
boolean dockerAvailable = true;
FakeCommands() { super(Duration.ofMinutes(1)); }
@Override Result execute(List<String> args, Path cwd, Map<String,String> env, String input, boolean echo) {
calls.add(List.copyOf(args));
if (args.getFirst().equals("git")) return new Result(0,SHA);
if (args.getFirst().equals("mvn") && mavenFailure) return new Result(42,"deliberate failure");
if (args.equals(List.of("npm", "run", "lint")) && lintFailure) return new Result(1,"deliberate lint failure");
if (!args.getFirst().equals("docker")) return new Result(0,"");
String command = args.get(1), ref = args.getLast();
if (command.equals("info")) return new Result(dockerAvailable ? 0 : 1,args.contains("{{.Architecture}}") ? "amd64" : "linux");
if (command.equals("context")) return new Result(0,"unix:///tmp/test-docker.sock");
if (command.equals("manifest")) {
if (manifestError) return new Result(1,"unauthorized");
return images.containsKey(ref) ? new Result(0,"{}") : new Result(1,"manifest unknown");
}
if (command.equals("tag")) tags.put(ref,images.getOrDefault(args.get(2),SHA));
if (command.equals("push")) {
if (!pushFailure.isEmpty() && ref.contains(pushFailure)) return new Result(1,"push failed");
images.put(ref,tags.getOrDefault(ref,SHA));
}
if (command.equals("pull") && !pullFailure.isEmpty() && ref.contains(pullFailure)) return new Result(1,"pull failed");
if (command.equals("image")) return new Result(0,args.stream().anyMatch(a -> a.contains("RepoDigests"))
? ref.substring(0,ref.lastIndexOf(':')) + "@sha256:" + "b".repeat(64) : images.getOrDefault(ref,"missing"));
if (command.equals("container") || command.equals("network") && args.contains("inspect")) return new Result(1,"");
return new Result(0,"");
}
}
}