Skip to content

Commit d7ee9dd

Browse files
Added assertion for matching lines count (#603)
* rs: count of matching lines added * cargo fmt * docs: count of matching lines added * ts: count of matching lines added * fix * prettier * test: count of matching lines added * bump versions Co-authored-by: Javier Viola <javier@parity.io>
1 parent 3a70912 commit d7ee9dd

File tree

12 files changed

+289
-5
lines changed

12 files changed

+289
-5
lines changed

Cargo.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/parser-wrapper/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "dsl-parser-wrapper"
3-
version = "0.1.6"
3+
version = "0.1.7"
44
edition = "2021"
55
description = "Zombienet DSL parser: produces a test definition, in json format, that can be used with the ZombieNet's test-runnner."
66
license = "GPL-3.0-or-later"

crates/parser/src/ast.rs

+9
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,15 @@ pub enum AssertionKind {
8080
#[serde(with = "optional_timeout")]
8181
timeout: Option<Duration>,
8282
},
83+
CountLogMatch {
84+
node_name: NodeName,
85+
match_type: String,
86+
pattern: String,
87+
op: Operator,
88+
target_value: u64,
89+
#[serde(with = "optional_timeout")]
90+
timeout: Option<Duration>,
91+
},
8392
Trace {
8493
node_name: NodeName,
8594
span_id: String,

crates/parser/src/lib.rs

+63
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,51 @@ fn parse_match_pattern_rule(
110110
Ok((name, match_type, pattern, timeout))
111111
}
112112

113+
fn parse_lines_count_match_pattern_rule(
114+
record: Pair<Rule>,
115+
) -> Result<(String, String, String, ast::Comparison, Option<Duration>), ParserError> {
116+
let mut pairs = record.into_inner();
117+
let name = parse_name(get_pair(&mut pairs, "name")?)?;
118+
119+
let mut explicit_match_type = false;
120+
121+
let pair = get_pair(&mut pairs, "match_type")?;
122+
let match_type = if let Rule::match_type = pair.as_rule() {
123+
explicit_match_type = true;
124+
pair.as_str().to_owned()
125+
} else {
126+
String::from("regex")
127+
};
128+
129+
let pattern_pair = if explicit_match_type {
130+
get_pair(&mut pairs, "pattern")?
131+
} else {
132+
pair
133+
};
134+
135+
let pattern = pattern_pair.as_str().trim_matches('"').to_owned();
136+
137+
let cmp_rule = get_pair(&mut pairs, "cmp_rule")?;
138+
let comparison: ast::Comparison = match cmp_rule.as_rule() {
139+
Rule::int => ast::Comparison {
140+
op: ast::Operator::Equal,
141+
target_value: parse_taget_value(cmp_rule)?,
142+
},
143+
Rule::comparison => parse_comparison(cmp_rule)?,
144+
_ => {
145+
return Err(ParserError::UnreachableRule(pairs.as_str().to_string()));
146+
}
147+
};
148+
149+
let timeout: Option<Duration> = if let Some(within_rule) = pairs.next() {
150+
Some(parse_within(within_rule)?)
151+
} else {
152+
None
153+
};
154+
155+
Ok((name, match_type, pattern, comparison, timeout))
156+
}
157+
113158
fn parse_custom_script_rule(record: Pair<Rule>, is_js: bool) -> Result<AssertionKind, ParserError> {
114159
let mut pairs = record.into_inner();
115160
let node_name = parse_name(get_pair(&mut pairs, "name")?)?;
@@ -405,6 +450,24 @@ pub fn parse(unparsed_file: &str) -> Result<ast::TestDefinition, errors::ParserE
405450

406451
assertions.push(assertion);
407452
}
453+
Rule::count_log_match => {
454+
let (name, match_type, pattern, comparison, timeout) =
455+
parse_lines_count_match_pattern_rule(record)?;
456+
457+
let assertion = Assertion {
458+
parsed: AssertionKind::CountLogMatch {
459+
node_name: name,
460+
match_type,
461+
pattern,
462+
target_value: comparison.target_value,
463+
op: comparison.op,
464+
timeout,
465+
},
466+
original_line,
467+
};
468+
469+
assertions.push(assertion);
470+
}
408471
Rule::trace => {
409472
// Pairs should be in order:
410473
// name, span_id, pattern, [timeout]

crates/parser/src/tests.rs

+124
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,130 @@ fn log_match_glob_parse_ok() {
307307
assert_eq!(result, t);
308308
}
309309

310+
#[test]
311+
fn count_log_match_equal_parse_ok() {
312+
let line: &str =
313+
r#"alice: count of log lines containing "Imported #12" is 0 within 20 seconds"#;
314+
let data = r#"{
315+
"description": null,
316+
"network": "./a.toml",
317+
"creds": "config",
318+
"assertions": [
319+
{
320+
"original_line": "alice: count of log lines containing \"Imported #12\" is 0 within 20 seconds",
321+
"parsed": {
322+
"fn": "CountLogMatch",
323+
"args": {
324+
"node_name": "alice",
325+
"match_type": "regex",
326+
"pattern": "Imported #12",
327+
"op": "Equal",
328+
"target_value": 0,
329+
"timeout": 20
330+
}
331+
}
332+
}
333+
]
334+
}"#;
335+
let t: TestDefinition = serde_json::from_str(data).unwrap();
336+
337+
let result = parse(&[NETWORK, CREDS, line].join("\n")).unwrap();
338+
assert_eq!(result, t);
339+
}
340+
341+
#[test]
342+
fn count_log_match_is_at_least_parse_ok() {
343+
let line: &str =
344+
r#"alice: count of log lines containing "Imported #12" is at least 12 within 20 seconds"#;
345+
let data = r#"{
346+
"description": null,
347+
"network": "./a.toml",
348+
"creds": "config",
349+
"assertions": [
350+
{
351+
"original_line": "alice: count of log lines containing \"Imported #12\" is at least 12 within 20 seconds",
352+
"parsed": {
353+
"fn": "CountLogMatch",
354+
"args": {
355+
"node_name": "alice",
356+
"match_type": "regex",
357+
"pattern": "Imported #12",
358+
"op": "IsAtLeast",
359+
"target_value": 12,
360+
"timeout": 20
361+
}
362+
}
363+
}
364+
]
365+
}"#;
366+
let t: TestDefinition = serde_json::from_str(data).unwrap();
367+
368+
let result = parse(&[NETWORK, CREDS, line].join("\n")).unwrap();
369+
assert_eq!(result, t);
370+
}
371+
372+
#[test]
373+
fn count_log_match_glob_equal_parse_ok() {
374+
let line: &str =
375+
r#"alice: count of log lines containing glob "Imported #12" is 10 within 20 seconds"#;
376+
let data = r#"{
377+
"description": null,
378+
"network": "./a.toml",
379+
"creds": "config",
380+
"assertions": [
381+
{
382+
"original_line": "alice: count of log lines containing glob \"Imported #12\" is 10 within 20 seconds",
383+
"parsed": {
384+
"fn": "CountLogMatch",
385+
"args": {
386+
"node_name": "alice",
387+
"match_type": "glob",
388+
"pattern": "Imported #12",
389+
"op": "Equal",
390+
"target_value": 10,
391+
"timeout": 20
392+
}
393+
}
394+
}
395+
]
396+
}"#;
397+
let t: TestDefinition = serde_json::from_str(data).unwrap();
398+
399+
let result = parse(&[NETWORK, CREDS, line].join("\n")).unwrap();
400+
assert_eq!(result, t);
401+
}
402+
403+
#[test]
404+
fn count_log_match_glob_is_at_least_parse_ok() {
405+
let line: &str =
406+
r#"alice: count of log lines matching glob "*rted #1*" is at least 5 within 10 seconds"#;
407+
let data = r#"{
408+
"description": null,
409+
"network": "./a.toml",
410+
"creds": "config",
411+
"assertions": [
412+
{
413+
"original_line": "alice: count of log lines matching glob \"*rted #1*\" is at least 5 within 10 seconds",
414+
"parsed": {
415+
"fn": "CountLogMatch",
416+
"args": {
417+
"node_name": "alice",
418+
"match_type": "glob",
419+
"pattern": "*rted #1*",
420+
"op": "IsAtLeast",
421+
"target_value": 5,
422+
"timeout": 10
423+
}
424+
}
425+
}
426+
]
427+
}"#;
428+
let t: TestDefinition = serde_json::from_str(data).unwrap();
429+
430+
let result = parse(&[NETWORK, CREDS, line].join("\n")).unwrap();
431+
assert_eq!(result, t);
432+
}
433+
310434
#[test]
311435
fn trace_parse_ok() {
312436
let line: &str = r#"alice: trace with traceID 94c1501a78a0d83c498cc92deec264d9 contains ["answer-chunk-request", "answer-chunk-request"]"#;

crates/parser/src/zombienet.pest

+3-1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ para_runtime_dummy_upgrade = { node_name ~ parachain ~ "perform dummy upgrade" ~
6464
histogram = { node_name ~ "reports histogram" ~ metric_name ~ "has" ~ (comparison | int+) ~ "samples in buckets" ~ square_brackets_strings ~ within? }
6565
report = { node_name ~ "reports" ~ metric_name ~ comparison ~ within? }
6666
log_match = { node_name ~ "log line" ~ ("contains"|"matches") ~ match_type? ~ double_quoted_string ~ within? }
67+
count_log_match = { node_name ~ "count of log lines" ~ ("containing"|"matching") ~ match_type? ~ double_quoted_string ~ "is" ~ (comparison | int+) ~ within? }
6768
trace = { node_name ~ "trace with traceID" ~ span_id ~ "contains" ~ square_brackets_strings ~ within? }
6869
system_event = { node_name ~ "system event" ~ ("contains"|"matches") ~ match_type? ~ double_quoted_string ~ within? }
6970
custom_js = { node_name ~ "js-script" ~ file_path ~ ("with" ~ double_quoted_string)? ~ ( "return" ~ comparison )? ~ within? }
@@ -93,6 +94,7 @@ file = { SOI ~ (
9394
histogram |
9495
report |
9596
log_match |
97+
count_log_match |
9698
trace |
9799
system_event |
98100
custom_js |
@@ -101,4 +103,4 @@ file = { SOI ~ (
101103
pause |
102104
resume |
103105
restart
104-
)* ~ NEWLINE* ~ EOI }
106+
)* ~ NEWLINE* ~ EOI }

docs/src/cli/test-dsl-definition-spec.md

+5
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ The first lines are used to define the **header fields**:
5555
- `node-name`: log line (contains|matches) ( regex|glob) "pattern" [within x seconds]
5656
- alice: log line matches glob "_rted #1_" within 10 seconds
5757

58+
- Logs assertions: Get logs from nodes and assert on the number of lines matching pattern (support `regex` and `glob`).
59+
60+
- `node-name`: count of log lines (containing|matcheing) ( regex|glob) "pattern" [within x seconds]
61+
- alice: count of log lines matching glob "_rted #1_" within 10 seconds
62+
5863
- System events assertion: Find a `system event` from subscription by matching a `pattern`. _NOTE_ the subscription is made when we start this particular test, so we **can not** match on event in the past.
5964

6065
- `node-name`: system event (contains|matches)( regex| glob) "pattern" [within x seconds]

docs/src/guide.md

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ alice: reports histogram polkadot_pvf_execution_time has at least 2 samples in b
7575
# logs
7676
bob: log line matches glob "*rted #1*" within 10 seconds
7777
bob: log line matches "Imported #[0-9]+" within 10 seconds
78+
bob: count of log lines maching "Error" is 0 within 10 seconds
7879
7980
# system events
8081
bob: system event contains "A candidate was included" within 20 seconds

javascript/packages/cli/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"url": "https://github.com/paritytech/zombienet/issues"
5050
},
5151
"dependencies": {
52-
"@zombienet/dsl-parser-wrapper": "^0.1.6",
52+
"@zombienet/dsl-parser-wrapper": "^0.1.7",
5353
"@zombienet/orchestrator": "^0.0.11",
5454
"@zombienet/utils": "^0.0.6",
5555
"axios": "^0.27.2",

javascript/packages/orchestrator/src/networkNode.ts

+51
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,57 @@ export class NetworkNode implements NetworkNodeInterface {
372372
}
373373
}
374374

375+
async countPatternLines(
376+
pattern: string,
377+
isGlob: boolean,
378+
timeout: number = DEFAULT_INDIVIDUAL_TEST_TIMEOUT,
379+
): Promise<number> {
380+
try {
381+
let total_count = 0;
382+
const re = isGlob ? minimatch.makeRe(pattern) : new RegExp(pattern, "ig");
383+
if (!re) throw new Error(`Invalid glob pattern: ${pattern} `);
384+
const client = getClient();
385+
const getValue = async (): Promise<number> => {
386+
await new Promise((resolve) => setTimeout(resolve, timeout * 1000));
387+
let logs = await client.getNodeLogs(this.name, undefined, true);
388+
389+
for (let line of logs.split("\n")) {
390+
if (client.providerName !== "native") {
391+
// remove the extra timestamp
392+
line = line.split(" ").slice(1).join(" ");
393+
}
394+
if (re.test(line)) {
395+
total_count += 1;
396+
}
397+
}
398+
return total_count;
399+
};
400+
401+
const resp = await Promise.race([
402+
getValue(),
403+
new Promise(
404+
(resolve) =>
405+
setTimeout(() => {
406+
const err = new Error(
407+
`Timeout(${timeout}), "getting log pattern ${pattern} within ${timeout} secs".`,
408+
);
409+
return resolve(err);
410+
}, (timeout + 2) * 1000), //extra 2s for processing log
411+
),
412+
]);
413+
if (resp instanceof Error) throw resp;
414+
415+
return total_count;
416+
} catch (err: any) {
417+
console.log(
418+
`\n\t ${decorators.red("Error: ")} \n\t\t ${decorators.red(
419+
err.message,
420+
)}\n`,
421+
);
422+
return 0;
423+
}
424+
}
425+
375426
async findPattern(
376427
pattern: string,
377428
isGlob: boolean,

javascript/packages/orchestrator/src/test-runner/assertions.ts

+26
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,31 @@ const LogMatch = ({ node_name, pattern, match_type, timeout }: FnArgs) => {
135135
};
136136
};
137137

138+
const CountLogMatch = ({
139+
node_name,
140+
pattern,
141+
match_type,
142+
op,
143+
target_value,
144+
timeout,
145+
}: FnArgs) => {
146+
const comparatorFn = comparators[op!];
147+
const isGlob = (match_type && match_type.trim() === "glob") || false;
148+
149+
return async (network: Network) => {
150+
const nodes = network.getNodes(node_name!);
151+
const results = await Promise.all(
152+
nodes.map((node: any) =>
153+
node.countPatternLines(pattern!, isGlob, timeout),
154+
),
155+
);
156+
157+
for (const value of results) {
158+
comparatorFn(value as number, target_value as number);
159+
}
160+
};
161+
};
162+
138163
const SystemEvent = ({ node_name, pattern, match_type, timeout }: FnArgs) => {
139164
const isGlob = (match_type && match_type.trim() === "glob") || false;
140165

@@ -412,6 +437,7 @@ export default {
412437
Histogram,
413438
Trace,
414439
LogMatch,
440+
CountLogMatch,
415441
SystemEvent,
416442
CustomJs,
417443
CustomSh,

0 commit comments

Comments
 (0)