use crate::model::Location;
use std::path::Path;
use yaml_rust2::parser::{Event, MarkedEventReceiver, Parser};
use yaml_rust2::scanner::Marker;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutlineSpan {
pub start_line: usize,
pub start_column: usize,
pub end_line: usize,
pub end_column: usize,
}
impl OutlineSpan {
pub fn point(line: usize, column: usize) -> Self {
Self {
start_line: line,
start_column: column,
end_line: line,
end_column: column,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StepOutline {
pub name: String,
pub range: OutlineSpan,
pub selection_range: OutlineSpan,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TestOutline {
pub name: String,
pub range: OutlineSpan,
pub selection_range: OutlineSpan,
pub steps: Vec<StepOutline>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Outline {
pub file: String,
pub file_name: Option<String>,
pub file_name_range: Option<OutlineSpan>,
pub setup: Vec<StepOutline>,
pub teardown: Vec<StepOutline>,
pub flat_steps: Vec<StepOutline>,
pub tests: Vec<TestOutline>,
}
pub fn outline_document(file: &Path, content: &str) -> Option<Outline> {
let mut sink = EventSink { events: Vec::new() };
let mut parser = Parser::new_from_str(content);
parser.load(&mut sink, true).ok()?;
let mut cursor = Cursor {
events: &sink.events,
pos: 0,
file: file.display().to_string(),
};
cursor.walk_document()
}
pub fn outline_from_str(file: &str, content: &str) -> Option<Outline> {
outline_document(Path::new(file), content)
}
struct EventSink {
events: Vec<(Event, Marker)>,
}
impl MarkedEventReceiver for EventSink {
fn on_event(&mut self, ev: Event, mark: Marker) {
self.events.push((ev, mark));
}
}
struct Cursor<'a> {
events: &'a [(Event, Marker)],
pos: usize,
file: String,
}
impl<'a> Cursor<'a> {
fn peek(&self) -> Option<&'a (Event, Marker)> {
self.events.get(self.pos)
}
fn advance(&mut self) -> Option<&'a (Event, Marker)> {
let event = self.events.get(self.pos);
if event.is_some() {
self.pos += 1;
}
event
}
fn point_span(mark: &Marker) -> OutlineSpan {
OutlineSpan::point(mark.line(), mark.col() + 1)
}
fn span_between(start: &Marker, end: &Marker) -> OutlineSpan {
OutlineSpan {
start_line: start.line(),
start_column: start.col() + 1,
end_line: end.line(),
end_column: end.col() + 1,
}
}
fn walk_document(&mut self) -> Option<Outline> {
match self.advance()? {
(Event::StreamStart, _) => {}
_ => return None,
}
match self.advance()? {
(Event::DocumentStart, _) => {}
_ => return None,
}
match self.advance()? {
(Event::MappingStart(_, _), _) => {}
_ => return None,
}
let mut outline = Outline {
file: self.file.clone(),
..Outline::default()
};
loop {
match self.peek()? {
(Event::MappingEnd, _) => {
self.advance();
break;
}
_ => {
let (key, _) = self.read_scalar_key_with_mark()?;
match key.as_str() {
"name" => {
let (event, mark) = self.advance()?;
match event {
Event::Scalar(value, _, _, _) => {
outline.file_name = Some(value.clone());
outline.file_name_range = Some(Self::point_span(mark));
}
_ => {
self.skip_remaining_node(event)?;
}
}
}
"setup" => {
outline.setup = self.walk_step_sequence()?;
}
"teardown" => {
outline.teardown = self.walk_step_sequence()?;
}
"steps" => {
outline.flat_steps = self.walk_step_sequence()?;
}
"tests" => {
outline.tests = self.walk_tests_mapping()?;
}
_ => {
self.skip_node()?;
}
}
}
}
}
Some(outline)
}
fn read_scalar_key_with_mark(&mut self) -> Option<(String, Marker)> {
let (event, mark) = self.advance()?;
match event {
Event::Scalar(value, _, _, _) => Some((value.clone(), *mark)),
_ => None,
}
}
fn skip_node(&mut self) -> Option<()> {
let (event, _) = self.advance()?;
self.skip_remaining_node(event)
}
fn skip_remaining_node(&mut self, event: &Event) -> Option<()> {
match event {
Event::Scalar(_, _, _, _) | Event::Alias(_) => Some(()),
Event::SequenceStart(_, _) => loop {
match self.peek()? {
(Event::SequenceEnd, _) => {
self.advance();
return Some(());
}
_ => {
self.skip_node()?;
}
}
},
Event::MappingStart(_, _) => loop {
match self.peek()? {
(Event::MappingEnd, _) => {
self.advance();
return Some(());
}
_ => {
self.skip_node()?; self.skip_node()?; }
}
},
_ => None,
}
}
fn walk_step_sequence(&mut self) -> Option<Vec<StepOutline>> {
match self.advance()? {
(Event::SequenceStart(_, _), _) => {}
_ => return Some(Vec::new()),
}
let mut items = Vec::new();
let mut index = 0usize;
loop {
match self.peek()? {
(Event::SequenceEnd, _) => {
self.advance();
return Some(items);
}
(Event::MappingStart(_, _), _) => {
index += 1;
items.push(self.walk_step_mapping(index)?);
}
_ => {
self.skip_node()?;
}
}
}
}
fn walk_step_mapping(&mut self, index: usize) -> Option<StepOutline> {
let start_mark = match self.advance()? {
(Event::MappingStart(_, _), mark) => *mark,
_ => return None,
};
let mut step_name: Option<String> = None;
let mut selection: Option<OutlineSpan> = None;
let end_mark: Marker;
loop {
match self.peek()? {
(Event::MappingEnd, mark) => {
end_mark = *mark;
self.advance();
break;
}
_ => {
let (key, key_mark) = self.read_scalar_key_with_mark()?;
if key == "name" {
let (event, mark) = self.advance()?;
match event {
Event::Scalar(value, _, _, _) => {
step_name = Some(value.clone());
selection = Some(Self::point_span(mark));
}
other => {
self.skip_remaining_node(other)?;
}
}
if selection.is_none() {
selection = Some(Self::point_span(&key_mark));
}
} else {
self.skip_node()?;
}
}
}
}
let display_name = step_name.unwrap_or_else(|| format!("<step {index}>"));
let range = Self::span_between(&start_mark, &end_mark);
let selection_range = selection.unwrap_or_else(|| Self::point_span(&start_mark));
Some(StepOutline {
name: display_name,
range,
selection_range,
})
}
fn walk_tests_mapping(&mut self) -> Option<Vec<TestOutline>> {
match self.advance()? {
(Event::MappingStart(_, _), _) => {}
_ => return Some(Vec::new()),
}
let mut groups = Vec::new();
loop {
match self.peek()? {
(Event::MappingEnd, _) => {
self.advance();
return Some(groups);
}
_ => {
let (name, key_mark) = self.read_scalar_key_with_mark()?;
let selection_range = Self::point_span(&key_mark);
if let Some((range, steps)) = self.walk_test_group_mapping(&key_mark)? {
groups.push(TestOutline {
name,
range,
selection_range,
steps,
});
}
}
}
}
}
fn walk_test_group_mapping(
&mut self,
key_mark: &Marker,
) -> Option<Option<(OutlineSpan, Vec<StepOutline>)>> {
match self.peek()? {
(Event::MappingStart(_, _), _) => {}
_ => {
let point = Self::point_span(key_mark);
self.skip_node()?;
return Some(Some((point, Vec::new())));
}
}
self.advance();
let mut steps = Vec::new();
let end_mark: Marker;
loop {
match self.peek()? {
(Event::MappingEnd, mark) => {
end_mark = *mark;
self.advance();
break;
}
_ => {
let (key, _) = self.read_scalar_key_with_mark()?;
if key == "steps" {
steps = self.walk_step_sequence()?;
} else {
self.skip_node()?;
}
}
}
}
Some(Some((Self::span_between(key_mark, &end_mark), steps)))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CaptureScope<'a> {
Setup,
Teardown,
FlatSteps,
Test(&'a str),
Any,
}
pub fn find_capture_declarations(
content: &str,
display_path: &str,
capture_name: &str,
scope: &CaptureScope<'_>,
) -> Vec<Location> {
let mut out = Vec::new();
let mut sink = EventSink { events: Vec::new() };
let mut parser = Parser::new_from_str(content);
if parser.load(&mut sink, true).is_err() {
return out;
}
let events = &sink.events;
let mut i = 0usize;
while i < events.len() {
if matches!(events[i].0, Event::MappingStart(_, _)) {
i += 1;
break;
}
i += 1;
}
while i < events.len() {
match &events[i].0 {
Event::MappingEnd => break,
Event::Scalar(key, _, _, _) => {
let key = key.clone();
i += 1;
let interested = match scope {
CaptureScope::Setup => key == "setup",
CaptureScope::Teardown => key == "teardown",
CaptureScope::FlatSteps => key == "steps",
CaptureScope::Test(_) => key == "tests",
CaptureScope::Any => {
key == "setup" || key == "teardown" || key == "steps" || key == "tests"
}
};
if !interested {
i = skip_event_node(events, i);
continue;
}
if key == "tests" {
let target = match scope {
CaptureScope::Test(name) => Some(*name),
_ => None,
};
i = scan_tests_for_captures(
events,
i,
display_path,
capture_name,
target,
&mut out,
);
} else {
i = scan_step_sequence_for_captures(
events,
i,
display_path,
capture_name,
&mut out,
);
}
}
_ => {
i = skip_event_node(events, i);
}
}
}
out
}
fn scan_tests_for_captures(
events: &[(Event, Marker)],
mut i: usize,
display_path: &str,
capture_name: &str,
target: Option<&str>,
out: &mut Vec<Location>,
) -> usize {
if !matches!(
events.get(i).map(|(e, _)| e),
Some(Event::MappingStart(_, _))
) {
return skip_event_node(events, i);
}
i += 1;
while i < events.len() {
match &events[i].0 {
Event::MappingEnd => return i + 1,
Event::Scalar(name, _, _, _) => {
let name = name.clone();
i += 1;
if !matches!(
events.get(i).map(|(e, _)| e),
Some(Event::MappingStart(_, _))
) {
i = skip_event_node(events, i);
continue;
}
let matches_target = target.map(|t| t == name).unwrap_or(true);
if !matches_target {
i = skip_event_node(events, i);
continue;
}
i += 1; while i < events.len() {
match &events[i].0 {
Event::MappingEnd => {
i += 1;
break;
}
Event::Scalar(inner_key, _, _, _) => {
let inner_key = inner_key.clone();
i += 1;
if inner_key == "steps" {
i = scan_step_sequence_for_captures(
events,
i,
display_path,
capture_name,
out,
);
} else {
i = skip_event_node(events, i);
}
}
_ => {
i = skip_event_node(events, i);
}
}
}
}
_ => {
i = skip_event_node(events, i);
}
}
}
i
}
fn scan_step_sequence_for_captures(
events: &[(Event, Marker)],
mut i: usize,
display_path: &str,
capture_name: &str,
out: &mut Vec<Location>,
) -> usize {
if !matches!(
events.get(i).map(|(e, _)| e),
Some(Event::SequenceStart(_, _))
) {
return skip_event_node(events, i);
}
i += 1;
while i < events.len() {
match &events[i].0 {
Event::SequenceEnd => return i + 1,
Event::MappingStart(_, _) => {
i += 1;
while i < events.len() {
match &events[i].0 {
Event::MappingEnd => {
i += 1;
break;
}
Event::Scalar(key, _, _, _) => {
let key = key.clone();
i += 1;
if key == "capture" {
i = scan_capture_mapping(
events,
i,
display_path,
capture_name,
out,
);
} else {
i = skip_event_node(events, i);
}
}
_ => {
i = skip_event_node(events, i);
}
}
}
}
_ => {
i = skip_event_node(events, i);
}
}
}
i
}
fn scan_capture_mapping(
events: &[(Event, Marker)],
mut i: usize,
display_path: &str,
capture_name: &str,
out: &mut Vec<Location>,
) -> usize {
if !matches!(
events.get(i).map(|(e, _)| e),
Some(Event::MappingStart(_, _))
) {
return skip_event_node(events, i);
}
i += 1;
while i < events.len() {
match &events[i].0 {
Event::MappingEnd => return i + 1,
Event::Scalar(key, _, _, _) => {
let key = key.clone();
let marker = events[i].1;
i += 1;
if key == capture_name {
out.push(Location {
file: display_path.to_owned(),
line: marker.line(),
column: marker.col() + 1,
});
}
i = skip_event_node(events, i);
}
_ => {
i = skip_event_node(events, i);
}
}
}
i
}
fn skip_event_node(events: &[(Event, Marker)], mut i: usize) -> usize {
if i >= events.len() {
return i;
}
let start = &events[i].0;
match start {
Event::Scalar(_, _, _, _) | Event::Alias(_) => i + 1,
Event::SequenceStart(_, _) => {
i += 1;
let mut depth = 1i32;
while i < events.len() && depth > 0 {
match &events[i].0 {
Event::SequenceStart(_, _) | Event::MappingStart(_, _) => depth += 1,
Event::SequenceEnd | Event::MappingEnd => depth -= 1,
_ => {}
}
i += 1;
}
i
}
Event::MappingStart(_, _) => {
i += 1;
let mut depth = 1i32;
while i < events.len() && depth > 0 {
match &events[i].0 {
Event::MappingStart(_, _) | Event::SequenceStart(_, _) => depth += 1,
Event::MappingEnd | Event::SequenceEnd => depth -= 1,
_ => {}
}
i += 1;
}
i
}
_ => i + 1,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathSegment {
Key(String),
Index(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScalarStyle {
Plain,
SingleQuoted,
DoubleQuoted,
Literal,
Folded,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScalarAtPosition {
pub value: String,
pub style: ScalarStyle,
pub path: Vec<PathSegment>,
pub start_line: usize,
pub start_column: usize,
pub end_line: usize,
pub end_column: usize,
pub start_byte: usize,
pub end_byte: usize,
}
pub fn find_scalar_at_position(
content: &str,
line_one_based: usize,
column_one_based: usize,
) -> Option<ScalarAtPosition> {
let mut sink = EventSink { events: Vec::new() };
let mut parser = Parser::new_from_str(content);
parser.load(&mut sink, true).ok()?;
let events = &sink.events;
if events.is_empty() {
return None;
}
let mut i = 0usize;
while i < events.len() {
match events[i].0 {
Event::StreamStart | Event::DocumentStart => i += 1,
_ => break,
}
}
let mut walker = ScalarWalker {
events,
source: content,
target_line: line_one_based,
target_col: column_one_based,
path: Vec::new(),
best: None,
};
walker.walk_node(&mut i);
walker.best
}
struct ScalarWalker<'a> {
events: &'a [(Event, Marker)],
source: &'a str,
target_line: usize,
target_col: usize,
path: Vec<PathSegment>,
best: Option<ScalarAtPosition>,
}
impl<'a> ScalarWalker<'a> {
fn walk_node(&mut self, i: &mut usize) {
if *i >= self.events.len() {
return;
}
let (event, mark) = &self.events[*i];
match event {
Event::Scalar(value, style, _, _) => {
let scalar_start_index = mark.index();
let scalar_start_line = mark.line();
let scalar_start_col = mark.col() + 1;
let (end_byte, end_line, end_col) =
scalar_end_position(self.source, scalar_start_index, value, *style);
if position_in_span(
self.target_line,
self.target_col,
scalar_start_line,
scalar_start_col,
end_line,
end_col,
) {
self.best = Some(ScalarAtPosition {
value: value.clone(),
style: tscalar_style_to_local(*style),
path: self.path.clone(),
start_line: scalar_start_line,
start_column: scalar_start_col,
end_line,
end_column: end_col,
start_byte: scalar_start_index,
end_byte,
});
}
*i += 1;
}
Event::MappingStart(_, _) => {
*i += 1;
loop {
match self.events.get(*i).map(|(e, _)| e) {
Some(Event::MappingEnd) => {
*i += 1;
break;
}
None => break,
_ => {
let key_event = &self.events[*i];
let key = match &key_event.0 {
Event::Scalar(k, _, _, _) => Some(k.clone()),
_ => None,
};
self.walk_node(i);
if let Some(key) = key {
self.path.push(PathSegment::Key(key));
self.walk_node(i);
self.path.pop();
} else {
self.walk_node(i);
}
}
}
}
}
Event::SequenceStart(_, _) => {
*i += 1;
let mut idx = 0usize;
loop {
match self.events.get(*i).map(|(e, _)| e) {
Some(Event::SequenceEnd) => {
*i += 1;
break;
}
None => break,
_ => {
self.path.push(PathSegment::Index(idx));
self.walk_node(i);
self.path.pop();
idx += 1;
}
}
}
}
Event::Alias(_) | Event::StreamEnd | Event::DocumentEnd => {
*i += 1;
}
_ => {
*i += 1;
}
}
}
}
fn scalar_end_position(
source: &str,
start_byte: usize,
value: &str,
style: yaml_rust2::scanner::TScalarStyle,
) -> (usize, usize, usize) {
use yaml_rust2::scanner::TScalarStyle;
let bytes = source.as_bytes();
let len = bytes.len();
if start_byte >= len {
let (line, col) = line_col_for_byte(source, start_byte.min(len));
return (start_byte.min(len), line, col);
}
let end_byte = match style {
TScalarStyle::DoubleQuoted => {
if bytes[start_byte] != b'"' {
plain_scalar_end(bytes, start_byte, value)
} else {
let mut j = start_byte + 1;
while j < len {
let b = bytes[j];
if b == b'\\' && j + 1 < len {
j += 2;
continue;
}
if b == b'"' {
j += 1;
break;
}
j += 1;
}
j
}
}
TScalarStyle::SingleQuoted => {
if bytes[start_byte] != b'\'' {
plain_scalar_end(bytes, start_byte, value)
} else {
let mut j = start_byte + 1;
while j < len {
let b = bytes[j];
if b == b'\'' {
if j + 1 < len && bytes[j + 1] == b'\'' {
j += 2;
continue;
}
j += 1;
break;
}
j += 1;
}
j
}
}
_ => plain_scalar_end(bytes, start_byte, value),
};
let clamped = end_byte.min(len);
let (line, col) = line_col_for_byte(source, clamped);
(clamped, line, col)
}
fn plain_scalar_end(bytes: &[u8], start_byte: usize, value: &str) -> usize {
let len = bytes.len();
let vbytes = value.as_bytes();
if start_byte + vbytes.len() <= len && &bytes[start_byte..start_byte + vbytes.len()] == vbytes {
return start_byte + vbytes.len();
}
let mut j = start_byte;
while j < len {
let b = bytes[j];
if b == b'\n' || b == b'#' {
break;
}
j += 1;
}
j
}
fn tscalar_style_to_local(style: yaml_rust2::scanner::TScalarStyle) -> ScalarStyle {
use yaml_rust2::scanner::TScalarStyle;
match style {
TScalarStyle::Plain => ScalarStyle::Plain,
TScalarStyle::SingleQuoted => ScalarStyle::SingleQuoted,
TScalarStyle::DoubleQuoted => ScalarStyle::DoubleQuoted,
TScalarStyle::Literal => ScalarStyle::Literal,
TScalarStyle::Folded => ScalarStyle::Folded,
}
}
fn line_col_for_byte(source: &str, byte: usize) -> (usize, usize) {
let clamped = byte.min(source.len());
let mut line = 1usize;
let mut col = 1usize;
for (i, c) in source.char_indices() {
if i >= clamped {
break;
}
if c == '\n' {
line += 1;
col = 1;
} else {
col += 1;
}
}
(line, col)
}
fn position_in_span(
target_line: usize,
target_col: usize,
start_line: usize,
start_col: usize,
end_line: usize,
end_col: usize,
) -> bool {
if target_line < start_line || target_line > end_line {
return false;
}
if target_line == start_line && target_col < start_col {
return false;
}
if target_line == end_line && target_col > end_col {
return false;
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_file_returns_none() {
let outline = outline_from_str("empty.tarn.yaml", "");
if let Some(outline) = outline {
assert!(outline.file_name.is_none());
assert!(outline.tests.is_empty());
assert!(outline.flat_steps.is_empty());
assert!(outline.setup.is_empty());
assert!(outline.teardown.is_empty());
}
}
#[test]
fn file_with_only_name_returns_file_level_name_only() {
let yaml = "name: Only\n";
let outline = outline_from_str("only.tarn.yaml", yaml).expect("outline");
assert_eq!(outline.file_name.as_deref(), Some("Only"));
assert!(outline.file_name_range.is_some());
assert!(outline.tests.is_empty());
assert!(outline.flat_steps.is_empty());
}
#[test]
fn flat_steps_produce_one_symbol_per_entry_with_full_ranges() {
let yaml = "\
name: Flat
steps:
- name: first
request:
method: GET
url: http://localhost/a
- name: second
request:
method: GET
url: http://localhost/b
assert:
status: 200
";
let outline = outline_from_str("flat.tarn.yaml", yaml).expect("outline");
assert_eq!(outline.flat_steps.len(), 2);
assert_eq!(outline.flat_steps[0].name, "first");
assert_eq!(outline.flat_steps[1].name, "second");
for step in &outline.flat_steps {
assert!(step.range.end_line >= step.range.start_line);
assert!(step.selection_range.start_line >= step.range.start_line);
assert!(step.selection_range.end_line <= step.range.end_line);
}
let first = &outline.flat_steps[0];
assert_eq!(first.selection_range.start_line, 3, "name on line 3");
}
#[test]
fn named_tests_produce_nested_step_symbols() {
let yaml = "\
name: Nested
tests:
group_a:
steps:
- name: alpha
request:
method: GET
url: http://localhost/a
- name: beta
request:
method: GET
url: http://localhost/b
group_b:
steps:
- name: gamma
request:
method: GET
url: http://localhost/c
";
let outline = outline_from_str("nested.tarn.yaml", yaml).expect("outline");
assert_eq!(outline.file_name.as_deref(), Some("Nested"));
assert_eq!(outline.tests.len(), 2);
let group_a = &outline.tests[0];
assert_eq!(group_a.name, "group_a");
assert_eq!(group_a.steps.len(), 2);
assert_eq!(group_a.steps[0].name, "alpha");
assert_eq!(group_a.steps[1].name, "beta");
let group_b = &outline.tests[1];
assert_eq!(group_b.name, "group_b");
assert_eq!(group_b.steps.len(), 1);
assert_eq!(group_b.steps[0].name, "gamma");
}
#[test]
fn setup_and_teardown_are_captured_independently() {
let yaml = "\
name: Hooks
setup:
- name: login
request:
method: POST
url: http://localhost/auth
teardown:
- name: cleanup
request:
method: POST
url: http://localhost/cleanup
steps:
- name: main
request:
method: GET
url: http://localhost/
";
let outline = outline_from_str("hooks.tarn.yaml", yaml).expect("outline");
assert_eq!(outline.setup.len(), 1);
assert_eq!(outline.teardown.len(), 1);
assert_eq!(outline.flat_steps.len(), 1);
assert_eq!(outline.setup[0].name, "login");
assert_eq!(outline.teardown[0].name, "cleanup");
assert_eq!(outline.flat_steps[0].name, "main");
}
#[test]
fn include_entries_receive_synthetic_placeholder_name() {
let yaml = "\
name: With include
setup:
- include: ./other.tarn.yaml
- name: real
request:
method: GET
url: http://localhost/
";
let outline = outline_from_str("inc.tarn.yaml", yaml).expect("outline");
assert_eq!(outline.setup.len(), 2);
assert_eq!(outline.setup[0].name, "<step 1>");
assert_eq!(outline.setup[1].name, "real");
}
#[test]
fn malformed_yaml_returns_none_without_panicking() {
let yaml = "name: broken\n bad-indent: true\n - list-here: oops\n";
let _ = outline_from_str("bad.tarn.yaml", yaml);
}
#[test]
fn step_range_end_is_after_start() {
let yaml = "\
name: Spans
steps:
- name: only
request:
method: GET
url: http://localhost/
";
let outline = outline_from_str("spans.tarn.yaml", yaml).expect("outline");
let step = &outline.flat_steps[0];
assert!(
step.range.end_line >= step.range.start_line,
"end_line must not precede start_line"
);
assert_eq!(step.name, "only");
}
#[test]
fn file_preserves_test_group_order_in_source() {
let yaml = "\
name: Order
tests:
zzz:
steps:
- name: last
request:
method: GET
url: http://localhost/z
aaa:
steps:
- name: first
request:
method: GET
url: http://localhost/a
";
let outline = outline_from_str("order.tarn.yaml", yaml).expect("outline");
assert_eq!(outline.tests[0].name, "zzz");
assert_eq!(outline.tests[1].name, "aaa");
}
#[test]
fn file_name_absent_yields_none_file_name() {
let yaml = "\
steps:
- name: s
request:
method: GET
url: http://localhost/
";
let outline = outline_from_str("n.tarn.yaml", yaml).expect("outline");
assert!(outline.file_name.is_none());
assert!(outline.file_name_range.is_none());
assert_eq!(outline.flat_steps.len(), 1);
}
#[test]
fn point_span_is_zero_width_and_sits_on_exact_marker() {
let span = OutlineSpan::point(3, 5);
assert_eq!(span.start_line, 3);
assert_eq!(span.end_line, 3);
assert_eq!(span.start_column, 5);
assert_eq!(span.end_column, 5);
}
#[test]
fn find_capture_declarations_locates_key_in_test_scope() {
let yaml = "\
name: Captures
tests:
main:
steps:
- name: login
request:
method: POST
url: http://x/auth
capture:
token: $.id
- name: next
request:
method: GET
url: http://x/items
";
let locs =
find_capture_declarations(yaml, "t.tarn.yaml", "token", &CaptureScope::Test("main"));
assert_eq!(locs.len(), 1);
assert_eq!(locs[0].line, 10);
assert_eq!(locs[0].file, "t.tarn.yaml");
}
#[test]
fn find_capture_declarations_locates_keys_in_flat_steps() {
let yaml = "\
name: Flat captures
steps:
- name: s1
request:
method: POST
url: http://x/
capture:
token: $.id
";
let locs =
find_capture_declarations(yaml, "t.tarn.yaml", "token", &CaptureScope::FlatSteps);
assert_eq!(locs.len(), 1);
assert_eq!(locs[0].line, 8);
}
#[test]
fn find_capture_declarations_returns_empty_when_name_missing() {
let yaml = "\
name: Missing
steps:
- name: s1
request:
method: GET
url: http://x/
capture:
other: $.id
";
let locs =
find_capture_declarations(yaml, "t.tarn.yaml", "token", &CaptureScope::FlatSteps);
assert!(locs.is_empty());
}
#[test]
fn find_capture_declarations_returns_all_occurrences_in_same_test() {
let yaml = "\
name: Dup
tests:
main:
steps:
- name: s1
request:
method: POST
url: http://x/a
capture:
token: $.id
- name: s2
request:
method: POST
url: http://x/b
capture:
token: $.id
";
let locs =
find_capture_declarations(yaml, "t.tarn.yaml", "token", &CaptureScope::Test("main"));
assert_eq!(locs.len(), 2);
assert!(locs[0].line < locs[1].line);
}
#[test]
fn find_capture_declarations_any_scope_searches_every_section() {
let yaml = "\
name: Any
setup:
- name: login
request:
method: POST
url: http://x/auth
capture:
session_id: $.id
teardown:
- name: cleanup
request:
method: DELETE
url: http://x/cleanup
capture:
deleted_id: $.id
steps:
- name: main
request:
method: GET
url: http://x/
capture:
main_id: $.id
";
let setup_locs =
find_capture_declarations(yaml, "t.tarn.yaml", "session_id", &CaptureScope::Any);
assert_eq!(setup_locs.len(), 1);
let teardown_locs =
find_capture_declarations(yaml, "t.tarn.yaml", "deleted_id", &CaptureScope::Any);
assert_eq!(teardown_locs.len(), 1);
let flat_locs =
find_capture_declarations(yaml, "t.tarn.yaml", "main_id", &CaptureScope::Any);
assert_eq!(flat_locs.len(), 1);
}
#[test]
fn find_capture_declarations_does_not_leak_across_named_tests() {
let yaml = "\
name: Two
tests:
first:
steps:
- name: a
request:
method: GET
url: http://x/a
capture:
token: $.id
second:
steps:
- name: b
request:
method: GET
url: http://x/b
capture:
token: $.id
";
let first =
find_capture_declarations(yaml, "t.tarn.yaml", "token", &CaptureScope::Test("first"));
assert_eq!(first.len(), 1);
let second =
find_capture_declarations(yaml, "t.tarn.yaml", "token", &CaptureScope::Test("second"));
assert_eq!(second.len(), 1);
assert_ne!(first[0].line, second[0].line);
}
#[test]
fn find_capture_declarations_malformed_yaml_returns_empty() {
let yaml = "name: bad\n - indent: [oops";
let locs = find_capture_declarations(yaml, "t.tarn.yaml", "x", &CaptureScope::Any);
assert!(locs.is_empty());
}
#[test]
fn find_scalar_at_position_returns_plain_url_with_path() {
let yaml = "\
steps:
- name: s
request:
method: GET
url: http://example.com/items
";
let scalar = find_scalar_at_position(yaml, 5, 15).expect("scalar");
assert_eq!(scalar.value, "http://example.com/items");
assert_eq!(scalar.style, ScalarStyle::Plain);
assert_eq!(
scalar.path,
vec![
PathSegment::Key("steps".into()),
PathSegment::Index(0),
PathSegment::Key("request".into()),
PathSegment::Key("url".into()),
]
);
}
#[test]
fn find_scalar_at_position_returns_quoted_url_with_quotes_in_span() {
let yaml = "\
steps:
- name: s
request:
method: GET
url: \"http://example.com/items\"
";
let scalar = find_scalar_at_position(yaml, 5, 20).expect("scalar");
assert_eq!(scalar.value, "http://example.com/items");
assert_eq!(scalar.style, ScalarStyle::DoubleQuoted);
let literal = &yaml[scalar.start_byte..scalar.end_byte];
assert_eq!(literal, "\"http://example.com/items\"");
}
#[test]
fn find_scalar_at_position_returns_none_on_whitespace() {
let yaml = "steps:\n - name: first\n request:\n url: http://x/\n";
assert!(find_scalar_at_position(yaml, 1, 50).is_none());
}
#[test]
fn find_scalar_at_position_reports_name_path_when_cursor_on_step_name() {
let yaml = "steps:\n - name: first\n request:\n url: http://x/\n";
let scalar = find_scalar_at_position(yaml, 2, 12).expect("scalar");
assert_eq!(scalar.value, "first");
assert_eq!(
scalar.path,
vec![
PathSegment::Key("steps".into()),
PathSegment::Index(0),
PathSegment::Key("name".into()),
]
);
}
#[test]
fn find_scalar_at_position_returns_none_for_malformed_yaml() {
let yaml = "steps: [oops\n bad";
assert!(find_scalar_at_position(yaml, 1, 5).is_none());
}
}