Repository navigation
Expand file tree
/
Copy pathScenario.java
More file actions
139 lines (114 loc) · 4.91 KB
/
Copy pathScenario.java
File metadata and controls
139 lines (114 loc) · 4.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
/*
* Scenario configuration (configs/<scenario>.json) and ${VAR} substitution.
*/
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Scenario {
static final ObjectMapper MAPPER = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
/** One scenario: processes to run (servers first) and what must hold afterwards. */
@JsonIgnoreProperties({ "comment" })
public record Config(String name, String description, Integer timeoutSec, List<String> peers,
List<ProcessSpec> processes, Assertions assertions) {
public Config {
peers = peers == null ? List.of() : peers;
assertions = assertions == null ? new Assertions(null, null, null, null, null, null) : assertions;
timeoutSec = timeoutSec == null ? 300 : timeoutSec;
}
}
/**
* A process. {@code role} is {@code server} (started first, waited for with {@code ready},
* killed at teardown) or {@code client} (run to completion within {@code timeoutSec}, must
* exit with {@code exitCode}, default 0). {@code samples} (servers only) take jcmd samples of
* the process at named moments.
*/
@JsonIgnoreProperties({ "comment" })
public record ProcessSpec(String name, String language, String role, String dir, String build, String run,
Map<String, String> env, Ready ready, Integer timeoutSec, Integer exitCode, List<SampleSpec> samples) {
public boolean isServer() {
return "server".equals(role);
}
public ProcessSpec {
env = env == null ? Map.of() : env;
samples = samples == null ? List.of() : samples;
timeoutSec = timeoutSec == null ? 120 : timeoutSec;
exitCode = exitCode == null ? 0 : exitCode;
}
}
/** Wait until a line containing {@code line} appears on the process's output. */
public record Ready(String line, Integer timeoutSec) {
public Ready {
timeoutSec = timeoutSec == null ? 60 : timeoutSec;
}
}
/**
* Take a sample labelled {@code label} when {@code on} happens: {@code ready} (this server
* became ready), {@code line} (process {@code process} printed a line containing
* {@code line}) or {@code exit} (process {@code process} exited), after {@code delayMs}.
*/
@JsonIgnoreProperties({ "comment" })
public record SampleSpec(String label, String on, String process, String line, Long delayMs) {
public SampleSpec {
delayMs = delayMs == null ? 0L : delayMs;
}
}
/**
* Deterministic assertions. Maps are keyed by process name. {@code checks} are
* {@code "<process> RESULT <lhs> <op> <rhs>"} or
* {@code "<process> SAMPLE <label> <lhs> <op> <rhs>"}; a term is a number, a key, or
* {@code <number>*<key>}. {@code report} substrings are counted and shown in the results,
* never asserted (for known, timing-dependent behaviour worth seeing).
*/
@JsonIgnoreProperties({ "comment" })
public record Assertions(Map<String, List<String>> requiredOutput, Map<String, List<String>> forbiddenOutput,
Map<String, List<String>> requiredPatterns, List<String> checks, List<ExpectedFailure> expectedFailures,
Map<String, List<String>> report) {
public Assertions {
requiredOutput = requiredOutput == null ? Map.of() : requiredOutput;
forbiddenOutput = forbiddenOutput == null ? Map.of() : forbiddenOutput;
requiredPatterns = requiredPatterns == null ? Map.of() : requiredPatterns;
checks = checks == null ? List.of() : checks;
expectedFailures = expectedFailures == null ? List.of() : expectedFailures;
report = report == null ? Map.of() : report;
}
}
/**
* A step that is known to fail: {@code STEP <step> FAIL} must appear on {@code process}'s
* output and contain {@code contains}. If the step passes instead, the scenario fails, so the
* expectation flips when the peer changes.
*/
public record ExpectedFailure(String process, String step, String contains, String reason, String see) {
}
public static Config load(Path file) throws Exception {
if (!Files.exists(file)) {
throw new IllegalArgumentException("Config not found: " + file);
}
return MAPPER.readValue(file.toFile(), Config.class);
}
private static final Pattern VAR = Pattern.compile("\\$\\{([A-Za-z0-9_.:-]+)}");
/** Replace every ${NAME}; an unknown name is an error, not an empty string. */
public static String subst(String s, Map<String, String> vars) {
if (s == null) {
return null;
}
Matcher m = VAR.matcher(s);
StringBuilder sb = new StringBuilder();
while (m.find()) {
String v = vars.get(m.group(1));
if (v == null) {
throw new IllegalArgumentException("Unknown variable ${" + m.group(1) + "} in: " + s
+ " (known: " + vars.keySet() + ")");
}
m.appendReplacement(sb, Matcher.quoteReplacement(v));
}
m.appendTail(sb);
return sb.toString();
}
}