Skip to content

Commit 6e99d62

Browse files
committed
test(coverage): cover 21 deterministic uncovered lines for --fail-under 100 gate
The CI coverage job reported 21 deterministic uncovered lines (identical across runs once RUST_TEST_THREADS=1 stabilised attribution). Add focused deterministic unit tests (NOT proptest-seed-dependent) driving each branch: - lsp: locator missing-constraint fallback, network simple types, drift models-load-failure path, inlay-hint non-scalar ref_columns, percent-decode valid triplet, find_all URI/range sort, classify_yaml non-scalar type walk, symbols table emission + YAML quoted-range trim - planner: check_expr_parser literal-start/keyword/eat_keyword arms, check_self_contradiction flatten_and leaf branch, schema PK-presence - cli/core/query: erd dot relationship label, normalize idempotence (fixed fixture), MySQL FK ON DELETE/ON UPDATE 3-backend rstest cargo test green across all 5 crates; fmt/clippy/line-budget clean.
1 parent 59deea7 commit 6e99d62

14 files changed

Lines changed: 341 additions & 5 deletions

File tree

crates/vespertide-cli/src/commands/erd/dot.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,27 @@ fn escape_record_field(value: &str) -> String {
8686

8787
escaped
8888
}
89+
90+
#[cfg(test)]
91+
mod tests {
92+
use super::*;
93+
use crate::commands::erd::Cardinality;
94+
95+
#[test]
96+
fn relationship_label_formats_many_to_many_relation_columns() {
97+
let relation = ForeignKeyRelation {
98+
child_table: "user_tag".to_string(),
99+
child_columns: vec!["user_id".to_string(), "tag_id".to_string()],
100+
parent_table: "tag".to_string(),
101+
parent_columns: vec!["id".to_string(), "tenant_id".to_string()],
102+
on_delete: None,
103+
on_update: None,
104+
cardinality: Cardinality::ManyToMany,
105+
};
106+
107+
assert_eq!(
108+
relationship_label(&relation),
109+
"M:N: user_id, tag_id -> id, tenant_id"
110+
);
111+
}
112+
}

crates/vespertide-core/src/schema/table/normalize_proptest.rs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33
use proptest::prelude::*;
44

55
use crate::arbitrary::arb_table_def;
6+
use crate::schema::primary_key::PrimaryKeySyntax;
7+
use crate::{ColumnDef, ColumnType, SimpleColumnType, StrOrBoolOrArray, TableDef};
8+
9+
fn normalize_again(table: &TableDef) -> TableDef {
10+
table
11+
.clone()
12+
.normalize()
13+
.expect("normalize must not fail on already-normalized output")
14+
}
615

716
proptest! {
817
#![proptest_config(ProptestConfig {
@@ -15,12 +24,30 @@ proptest! {
1524
#[test]
1625
fn normalize_is_idempotent(table in arb_table_def()) {
1726
if let Ok(once) = table.normalize() {
18-
let twice = once
19-
.clone()
20-
.normalize()
21-
.expect("normalize must not fail on already-normalized output");
27+
let twice = normalize_again(&once);
2228
prop_assert_eq!(once, twice);
2329
}
2430
// If normalize fails (e.g. duplicate index name), the property is vacuously true.
2531
}
2632
}
33+
34+
#[test]
35+
fn normalize_is_idempotent_for_fixed_inline_constraints() {
36+
let table = TableDef {
37+
name: "account".into(),
38+
description: None,
39+
columns: vec![
40+
ColumnDef::new("id", ColumnType::Simple(SimpleColumnType::Integer), false)
41+
.primary_key(PrimaryKeySyntax::Bool(true)),
42+
ColumnDef::new("email", ColumnType::Simple(SimpleColumnType::Text), false)
43+
.unique(StrOrBoolOrArray::Bool(true))
44+
.index(StrOrBoolOrArray::Bool(true)),
45+
],
46+
constraints: Vec::new(),
47+
};
48+
49+
let once = table.normalize().expect("fixed table should normalize");
50+
let twice = normalize_again(&once);
51+
52+
assert_eq!(once, twice);
53+
}

crates/vespertide-lsp/src/diagnostics/locator.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,16 @@ mod tests {
468468
assert!(src[range].contains("user"));
469469
}
470470

471+
#[test]
472+
fn locate_constraint_fallback_to_top_name_when_constraint_missing() {
473+
let pool = ParserPool::new();
474+
let src = r#"{"name":"user","constraints":[]}"#;
475+
let tree = pool.parse(src, DocumentFormat::Json).unwrap();
476+
let range = locate_constraint(Some(&tree), src, "missing_constraint");
477+
478+
assert!(src[range].contains("user"));
479+
}
480+
471481
#[test]
472482
fn locate_column_field_nullable_finds_target() {
473483
let pool = ParserPool::new();

crates/vespertide-lsp/src/diagnostics/validation/types.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,13 @@ mod tests {
237237
}
238238
}
239239

240+
#[test]
241+
fn known_simple_types_include_network_scalars() {
242+
assert!(KNOWN_SIMPLE_TYPES.contains(&"inet"));
243+
assert!(KNOWN_SIMPLE_TYPES.contains(&"cidr"));
244+
assert!(KNOWN_SIMPLE_TYPES.contains(&"macaddr"));
245+
}
246+
240247
#[test]
241248
fn enum_descriptor_skips_objects_missing_name_or_value_fields() {
242249
let src = r#"{"values":[{"value":1},{"name":"low"},{"name":"high","value":2}]}"#;

crates/vespertide-lsp/src/drift/compute.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,22 @@ mod tests {
261261
assert!(out.is_empty());
262262
}
263263

264+
#[test]
265+
fn compute_returns_empty_when_models_directory_load_fails() {
266+
let tmp = tempdir().unwrap();
267+
fs::create_dir_all(tmp.path().join("migrations")).unwrap();
268+
fs::write(tmp.path().join("vespertide.json"), r#"{"modelsDir":"models","migrationsDir":"migrations","tableNamingCase":"snake","columnNamingCase":"snake","modelFormat":"json"}"#).unwrap();
269+
270+
let out = compute_with_cache(
271+
tmp.path(),
272+
&WorkspaceIndex::new(),
273+
&DocumentStore::new(),
274+
&DriftCache::new(),
275+
);
276+
277+
assert!(out.is_empty());
278+
}
279+
264280
#[test]
265281
fn compute_returns_empty_when_diff_validation_fails() {
266282
let tmp = tempdir().unwrap();

crates/vespertide-lsp/src/inlay_hints.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,18 @@ mod tests {
621621
);
622622
}
623623

624+
#[test]
625+
fn yaml_block_sequence_ref_columns_skips_non_scalar_items() {
626+
let src = "name: post\ncolumns:\n - name: author_id\n type: integer\n foreign_key:\n ref_table: user\n ref_columns:\n - name: id\n";
627+
let tree = parse_yaml(src);
628+
let hints = compute(src, Some(&tree), 0..src.len());
629+
630+
assert!(
631+
hints.is_empty(),
632+
"mapping-valued ref_columns item must not produce FK hint: {hints:?}"
633+
);
634+
}
635+
624636
#[test]
625637
fn columns_value_that_is_not_sequence_yields_no_hints() {
626638
let src = r#"{"name":"u","columns":"not an array"}"#;

crates/vespertide-lsp/src/position.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,11 @@ mod tests {
261261
);
262262
}
263263

264+
#[test]
265+
fn percent_decode_consumes_valid_triplets_and_continues_after_them() {
266+
assert_eq!(percent_decode("/tmp/%41%2fname.json"), "/tmp/A/name.json");
267+
}
268+
264269
#[rstest]
265270
#[case::unencoded("file:///plain/path/file.json", &["plain", "file.json"])]
266271
#[case::lowercase_hex_drive("file:///c%3a/Users/test", &["Users"])]

crates/vespertide-lsp/src/references/search.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,33 @@ mod tests {
580580
);
581581
}
582582

583+
#[test]
584+
fn find_all_sorts_references_by_uri_then_byte_range() {
585+
let docs = DocumentStore::new();
586+
let current_uri = uri("z_post.json");
587+
let other_uri = uri("a_post.json");
588+
let src = r#"{"name":"post","columns":[{"name":"user_id","type":"integer","foreign_key":{"ref_table":"user","ref_columns":["id"]}}]}"#;
589+
let current_tree = parse_json(src);
590+
docs.open(other_uri.clone(), "json".to_string(), 1, src.to_string());
591+
592+
let refs = find_all(
593+
&ReferenceSymbol::Table {
594+
name: "user".to_string(),
595+
},
596+
&current_uri,
597+
src,
598+
Some(&current_tree),
599+
&WorkspaceIndex::new(),
600+
&docs,
601+
None,
602+
false,
603+
);
604+
605+
assert_eq!(refs.len(), 2);
606+
assert_eq!(refs[0].uri, other_uri);
607+
assert_eq!(refs[1].uri, current_uri);
608+
}
609+
583610
#[test]
584611
fn helper_false_paths_handle_missing_siblings_and_non_mapping_nodes() {
585612
let src = r#"{"name":"user","columns":[{"name":"id","type":"integer","foreign_key":{"ref_columns":["id"]}}],"constraints":[{"name":"c","expr":"id > 0"}]}"#;

crates/vespertide-lsp/src/semantic_tokens/classify_yaml.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,49 @@ mod tests {
644644
let _ = classify_yaml(malformed);
645645
}
646646

647+
#[test]
648+
fn classify_type_value_walks_non_scalar_non_mapping_values() {
649+
let pool = ParserPool::new();
650+
let src = "name: u\ncolumns:\n - name: x\n type:\n - integer\n";
651+
let tree = pool.parse(src, DocumentFormat::Yaml).unwrap();
652+
let type_pair = first_node(tree.root_node(), "block_mapping_pair")
653+
.and_then(|_| find_type_pair(tree.root_node(), src.as_bytes()))
654+
.expect("type pair");
655+
let value = type_pair.named_child(1).expect("type value");
656+
let mut out = Vec::new();
657+
658+
classify_type_value(value, src.as_bytes(), Ctx::default(), &mut out);
659+
660+
assert!(
661+
out.is_empty(),
662+
"sequence-valued type should be walked without type tokens: {out:?}"
663+
);
664+
}
665+
666+
fn find_type_pair<'tree>(
667+
node: tree_sitter::Node<'tree>,
668+
source: &[u8],
669+
) -> Option<tree_sitter::Node<'tree>> {
670+
if matches!(node.kind(), "block_mapping_pair" | "flow_pair")
671+
&& node
672+
.named_child(0)
673+
.and_then(|key| source.get(key.byte_range()))
674+
.and_then(|bytes| std::str::from_utf8(bytes).ok())
675+
.map(|text| text.trim().trim_matches('"').trim_matches('\''))
676+
== Some("type")
677+
{
678+
return Some(node);
679+
}
680+
681+
let mut cursor = node.walk();
682+
for child in node.children(&mut cursor) {
683+
if let Some(found) = find_type_pair(child, source) {
684+
return Some(found);
685+
}
686+
}
687+
None
688+
}
689+
647690
#[test]
648691
fn check_expr_emitter_returns_for_non_scalar_empty_and_bad_sources() {
649692
let pool = ParserPool::new();

crates/vespertide-lsp/src/symbols.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,19 @@ mod tests {
448448
assert_eq!(email.container.as_deref(), Some("user"));
449449
}
450450

451+
#[test]
452+
fn extract_raw_symbols_emits_table_symbol_with_identifier_range() {
453+
let pool = ParserPool::new();
454+
let src = r#"{"name":"user","columns":[]}"#;
455+
let tree = pool.parse(src, DocumentFormat::Json).unwrap();
456+
let symbols = extract_raw_symbols(&tree, src);
457+
458+
assert_eq!(symbols.len(), 1);
459+
assert_eq!(symbols[0].name, "user");
460+
assert_eq!(symbols[0].kind, SymbolKind::Table);
461+
assert_eq!(&src[symbols[0].byte_range.clone()], "user");
462+
}
463+
451464
#[test]
452465
fn query_filters_case_insensitively() {
453466
let docs = DocumentStore::new();
@@ -843,6 +856,26 @@ mod tests {
843856
assert_eq!(symbols.len(), 1);
844857
}
845858

859+
#[test]
860+
fn extract_raw_symbols_trims_yaml_quoted_identifier_ranges() {
861+
let pool = ParserPool::new();
862+
let src = "name: \"user\"\ncolumns:\n - name: 'id'\n type: integer\n";
863+
let tree = pool.parse(src, DocumentFormat::Yaml).unwrap();
864+
let symbols = extract_raw_symbols(&tree, src);
865+
866+
let table = symbols
867+
.iter()
868+
.find(|symbol| symbol.kind == SymbolKind::Table)
869+
.expect("table symbol");
870+
let column = symbols
871+
.iter()
872+
.find(|symbol| symbol.kind == SymbolKind::Column)
873+
.expect("column symbol");
874+
875+
assert_eq!(&src[table.byte_range.clone()], "user");
876+
assert_eq!(&src[column.byte_range.clone()], "id");
877+
}
878+
846879
#[test]
847880
fn unwrap_yaml_and_trim_one_byte_defensive_branches() {
848881
let pool = ParserPool::new();

0 commit comments

Comments
 (0)