1use std::io::{self, BufRead, IsTerminal};
2use std::path::{Path, PathBuf};
3
4use path_slash::PathExt as _;
5
6use crate::graph::types::LineageGraph;
7use crate::parser::project::ResolvedPaths;
8use crate::parser::yaml_schema;
9
10enum InputLine {
12 SqlFile(PathBuf),
14 YamlFile(PathBuf),
16 ModelName(String),
18 Ignore,
20}
21
22pub(crate) fn normalize_path(path: &Path) -> PathBuf {
26 if let Ok(canonical) = path.canonicalize() {
28 return canonical;
29 }
30 let mut components_to_append = Vec::new();
32 let mut current = path.to_path_buf();
33 loop {
34 if let Ok(canonical) = current.canonicalize() {
35 let mut result = canonical;
36 for component in components_to_append.into_iter().rev() {
37 result.push(component);
38 }
39 return result;
40 }
41 if let Some(file_name) = current.file_name() {
42 components_to_append.push(file_name.to_owned());
43 }
44 if !current.pop() {
45 break;
46 }
47 }
48 path.to_path_buf()
49}
50
51fn to_absolute(path_str: &str, cwd: &Path) -> PathBuf {
56 let path = Path::new(path_str);
57 if path.is_absolute() {
58 path.to_path_buf()
59 } else {
60 cwd.join(path)
61 }
62}
63
64pub fn read_stdin_lines() -> Vec<String> {
69 let stdin = io::stdin();
70 if stdin.is_terminal() {
71 return Vec::new();
72 }
73
74 #[cfg(unix)]
83 {
84 use std::os::unix::fs::FileTypeExt;
85 use std::os::unix::io::{AsRawFd, FromRawFd};
86 let ft = {
89 let f = std::mem::ManuallyDrop::new(unsafe {
90 std::fs::File::from_raw_fd(stdin.as_raw_fd())
91 });
92 match f.metadata() {
93 Ok(m) => m.file_type(),
94 Err(_) => return Vec::new(),
95 }
96 };
97 if !ft.is_fifo() && !ft.is_file() {
98 return Vec::new();
99 }
100 }
101
102 stdin
103 .lock()
104 .lines()
105 .map_while(|l| l.ok())
106 .filter(|l| !l.trim().is_empty())
107 .map(|l| l.trim().to_string())
108 .collect()
109}
110
111fn classify_line(line: &str, resolved_paths: &ResolvedPaths, cwd: &Path) -> InputLine {
114 let path = Path::new(line);
115 match path.extension().and_then(|e| e.to_str()) {
116 Some("sql") => {
117 let abs = normalize_path(&to_absolute(line, cwd));
118 if is_under_dbt_paths(&abs, resolved_paths) {
119 InputLine::SqlFile(abs)
120 } else {
121 InputLine::Ignore
122 }
123 }
124 Some("yml" | "yaml") => {
125 let abs = normalize_path(&to_absolute(line, cwd));
126 if is_under_dbt_paths(&abs, resolved_paths) {
127 InputLine::YamlFile(abs)
128 } else {
129 InputLine::Ignore
130 }
131 }
132 Some(ext) => {
133 if line.contains('/') || line.contains('\\') || is_common_file_extension(ext) {
139 InputLine::Ignore
140 } else {
141 InputLine::ModelName(line.to_string())
142 }
143 }
144 None => {
145 if line.contains('/') || line.contains('\\') {
148 InputLine::Ignore
149 } else {
150 InputLine::ModelName(line.to_string())
151 }
152 }
153 }
154}
155
156fn is_common_file_extension(ext: &str) -> bool {
166 matches!(
167 ext,
168 "md" | "txt"
169 | "py"
170 | "csv"
171 | "json"
172 | "toml"
173 | "cfg"
174 | "ini"
175 | "rst"
176 | "lock"
177 | "xml"
178 | "html"
179 | "htm"
180 | "js"
181 | "ts"
182 | "sh"
183 | "bat"
184 | "rs"
185 | "go"
186 | "java"
187 | "rb"
188 | "c"
189 | "h"
190 | "cpp"
191 | "hpp"
192 | "swift"
193 | "kt"
194 | "log"
195 | "env"
196 | "gitignore"
197 )
198}
199
200pub fn has_path_like_input(inputs: &[String]) -> bool {
204 inputs.iter().any(|s| {
205 s.contains('/')
206 || s.contains('\\')
207 || s.ends_with(".sql")
208 || s.ends_with(".yml")
209 || s.ends_with(".yaml")
210 })
211}
212
213fn is_under_dbt_paths(abs_path: &Path, resolved_paths: &ResolvedPaths) -> bool {
215 let abs_path = normalize_path(abs_path);
216 let all_paths = resolved_paths
217 .model_paths
218 .iter()
219 .chain(&resolved_paths.seed_paths)
220 .chain(&resolved_paths.snapshot_paths)
221 .chain(&resolved_paths.test_paths)
222 .chain(&resolved_paths.analysis_paths);
223
224 all_paths.into_iter().any(|dir| abs_path.starts_with(dir))
225}
226
227fn resolve_sql_to_label(
229 abs_path: &Path,
230 graph: &LineageGraph,
231 project_dir: &Path,
232) -> Option<String> {
233 let abs_path = normalize_path(abs_path);
234 let project_dir = normalize_path(project_dir);
235 let relative = abs_path.strip_prefix(&project_dir).ok()?;
236 let rel_str = relative.to_slash_lossy();
238
239 graph.node_indices().find_map(|idx| {
240 let node = &graph[idx];
241 match &node.file_path {
242 Some(node_path) => {
243 let node_str = node_path.to_slash_lossy();
244 if node_str == rel_str {
245 Some(node.label.clone())
246 } else {
247 None
248 }
249 }
250 None => None,
251 }
252 })
253}
254
255fn expand_yaml_names(abs_path: &Path) -> Vec<String> {
257 let content = match std::fs::read_to_string(abs_path) {
258 Ok(c) => c,
259 Err(e) => {
260 crate::warn!("could not read {}: {}", abs_path.display(), e);
261 return Vec::new();
262 }
263 };
264
265 let schema = match yaml_schema::parse_schema_file(&content, Some(abs_path)) {
266 Ok(s) => s,
267 Err(e) => {
268 crate::warn!("could not parse {}: {}", abs_path.display(), e);
269 return Vec::new();
270 }
271 };
272
273 let mut names = Vec::new();
274 for source in &schema.sources {
275 for table in &source.tables {
276 names.push(format!("{}.{}", source.name, table.name));
277 }
278 }
279 for model in &schema.models {
280 names.push(model.name.clone());
281 }
282 for sm in &schema.semantic_models {
283 names.push(format!("semantic_model.{}", sm.name));
284 }
285 for metric in &schema.metrics {
286 names.push(format!("metric.{}", metric.name));
287 }
288 for sq in &schema.saved_queries {
289 names.push(format!("saved_query.{}", sq.name));
290 }
291 names
292}
293
294pub fn resolve_stdin_inputs(
301 lines: &[String],
302 graph: &LineageGraph,
303 resolved_paths: &ResolvedPaths,
304 project_dir: &Path,
305 cwd: &Path,
306) -> Vec<String> {
307 let mut seen = std::collections::HashSet::new();
308 let mut names = Vec::new();
309
310 for line in lines {
311 match classify_line(line, resolved_paths, cwd) {
312 InputLine::SqlFile(abs_path) => {
313 if let Some(label) = resolve_sql_to_label(&abs_path, graph, project_dir) {
314 if seen.insert(label.clone()) {
315 names.push(label);
316 }
317 } else {
318 crate::warn!("no node found for file {}, skipping.", abs_path.display());
319 }
320 }
321 InputLine::YamlFile(abs_path) => {
322 for name in expand_yaml_names(&abs_path) {
323 if seen.insert(name.clone()) {
324 names.push(name);
325 }
326 }
327 }
328 InputLine::ModelName(name) => {
329 if seen.insert(name.clone()) {
330 names.push(name);
331 }
332 }
333 InputLine::Ignore => {}
334 }
335 }
336
337 names
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343 use crate::graph::types::{NodeData, NodeType};
344 use std::fs;
345
346 fn make_resolved_paths(project_dir: &Path) -> ResolvedPaths {
347 let norm = |name: &str| vec![normalize_path(&project_dir.join(name))];
348 ResolvedPaths {
349 model_paths: norm("models"),
350 seed_paths: norm("seeds"),
351 snapshot_paths: norm("snapshots"),
352 test_paths: norm("tests"),
353 macro_paths: norm("macros"),
354 analysis_paths: norm("analyses"),
355 }
356 }
357
358 fn make_node(unique_id: &str, label: &str, node_type: NodeType) -> NodeData {
359 NodeData {
360 unique_id: unique_id.to_string(),
361 label: label.to_string(),
362 node_type,
363 file_path: None,
364 description: None,
365 materialization: None,
366 tags: vec![],
367 columns: vec![],
368 exposure: None,
369 aliases: vec![],
370 }
371 }
372
373 #[test]
378 fn test_classify_sql_under_models() {
379 let tmp = tempfile::tempdir().unwrap();
380 let paths = make_resolved_paths(tmp.path());
381 let result = classify_line("models/staging/stg_orders.sql", &paths, tmp.path());
382 assert!(matches!(result, InputLine::SqlFile(_)));
383 }
384
385 #[test]
386 fn test_classify_sql_under_snapshots() {
387 let tmp = tempfile::tempdir().unwrap();
388 let paths = make_resolved_paths(tmp.path());
389 let result = classify_line("snapshots/snap_orders.sql", &paths, tmp.path());
390 assert!(matches!(result, InputLine::SqlFile(_)));
391 }
392
393 #[test]
394 fn test_classify_sql_under_analyses() {
395 let tmp = tempfile::tempdir().unwrap();
396 let paths = make_resolved_paths(tmp.path());
397 let result = classify_line("analyses/my_analysis.sql", &paths, tmp.path());
398 assert!(matches!(result, InputLine::SqlFile(_)));
399 }
400
401 #[test]
402 fn test_classify_sql_outside_dbt_paths() {
403 let tmp = tempfile::tempdir().unwrap();
404 let paths = make_resolved_paths(tmp.path());
405 let result = classify_line("other/script.sql", &paths, tmp.path());
406 assert!(matches!(result, InputLine::Ignore));
407 }
408
409 #[test]
410 fn test_classify_yml_under_models() {
411 let tmp = tempfile::tempdir().unwrap();
412 let paths = make_resolved_paths(tmp.path());
413 let result = classify_line("models/staging/schema.yml", &paths, tmp.path());
414 assert!(matches!(result, InputLine::YamlFile(_)));
415 }
416
417 #[test]
418 fn test_classify_yaml_under_models() {
419 let tmp = tempfile::tempdir().unwrap();
420 let paths = make_resolved_paths(tmp.path());
421 let result = classify_line("models/schema.yaml", &paths, tmp.path());
422 assert!(matches!(result, InputLine::YamlFile(_)));
423 }
424
425 #[test]
426 fn test_classify_yml_outside_dbt_paths() {
427 let tmp = tempfile::tempdir().unwrap();
428 let paths = make_resolved_paths(tmp.path());
429 let result = classify_line(".github/workflows/ci.yml", &paths, tmp.path());
430 assert!(matches!(result, InputLine::Ignore));
431 }
432
433 #[test]
434 fn test_classify_non_dbt_extension_with_separator() {
435 let tmp = tempfile::tempdir().unwrap();
436 let paths = make_resolved_paths(tmp.path());
437 assert!(matches!(
439 classify_line("seeds/data.csv", &paths, tmp.path()),
440 InputLine::Ignore
441 ));
442 assert!(matches!(
443 classify_line("models/model.py", &paths, tmp.path()),
444 InputLine::Ignore
445 ));
446 }
447
448 #[test]
449 fn test_classify_non_dbt_extension_without_separator() {
450 let tmp = tempfile::tempdir().unwrap();
451 let paths = make_resolved_paths(tmp.path());
452 assert!(matches!(
454 classify_line("README.md", &paths, tmp.path()),
455 InputLine::Ignore
456 ));
457 assert!(matches!(
458 classify_line("Cargo.toml", &paths, tmp.path()),
459 InputLine::Ignore
460 ));
461 assert!(matches!(
462 classify_line("setup.py", &paths, tmp.path()),
463 InputLine::Ignore
464 ));
465 }
466
467 #[test]
468 fn test_classify_no_extension() {
469 let tmp = tempfile::tempdir().unwrap();
470 let paths = make_resolved_paths(tmp.path());
471 let result = classify_line("stg_orders", &paths, tmp.path());
472 assert!(matches!(result, InputLine::ModelName(ref n) if n == "stg_orders"));
473 }
474
475 #[test]
476 fn test_classify_source_name() {
477 let tmp = tempfile::tempdir().unwrap();
478 let paths = make_resolved_paths(tmp.path());
479 let result = classify_line("raw.orders", &paths, tmp.path());
481 assert!(matches!(result, InputLine::ModelName(ref n) if n == "raw.orders"));
482 }
483
484 #[test]
487 fn test_is_under_dbt_paths_nested() {
488 let tmp = tempfile::tempdir().unwrap();
489 let paths = make_resolved_paths(tmp.path());
490 let abs = tmp.path().join("models/staging/stg_orders.sql");
491 assert!(is_under_dbt_paths(&abs, &paths));
492 }
493
494 #[test]
495 fn test_is_under_dbt_paths_absolute() {
496 let tmp = tempfile::tempdir().unwrap();
497 let paths = make_resolved_paths(tmp.path());
498 let abs = tmp.path().join("models/orders.sql");
499 assert!(is_under_dbt_paths(&abs, &paths));
500 }
501
502 #[test]
503 fn test_is_not_under_dbt_paths() {
504 let tmp = tempfile::tempdir().unwrap();
505 let paths = make_resolved_paths(tmp.path());
506 let abs = tmp.path().join("other/file.sql");
507 assert!(!is_under_dbt_paths(&abs, &paths));
508 }
509
510 #[test]
513 fn test_resolve_sql_to_label_found() {
514 let project_dir = Path::new("/project");
515 let mut graph = LineageGraph::new();
516 let mut node = make_node("model.stg_orders", "stg_orders", NodeType::Model);
517 node.file_path = Some(PathBuf::from("models/staging/stg_orders.sql"));
518 graph.add_node(node);
519
520 let abs = Path::new("/project/models/staging/stg_orders.sql");
521 let result = resolve_sql_to_label(abs, &graph, project_dir);
522 assert_eq!(result, Some("stg_orders".to_string()));
523 }
524
525 #[test]
526 fn test_resolve_sql_to_label_not_found() {
527 let project_dir = Path::new("/project");
528 let graph = LineageGraph::new();
529
530 let abs = Path::new("/project/models/nonexistent.sql");
531 let result = resolve_sql_to_label(abs, &graph, project_dir);
532 assert_eq!(result, None);
533 }
534
535 #[test]
538 fn test_expand_yaml_sources() {
539 let tmp = tempfile::tempdir().unwrap();
540 let yaml_path = tmp.path().join("schema.yml");
541 fs::write(
542 &yaml_path,
543 r#"
544sources:
545 - name: raw
546 tables:
547 - name: orders
548 - name: customers
549"#,
550 )
551 .unwrap();
552
553 let names = expand_yaml_names(&yaml_path);
554 assert_eq!(names, vec!["raw.orders", "raw.customers"]);
555 }
556
557 #[test]
558 fn test_expand_yaml_models() {
559 let tmp = tempfile::tempdir().unwrap();
560 let yaml_path = tmp.path().join("schema.yml");
561 fs::write(
562 &yaml_path,
563 r#"
564models:
565 - name: stg_orders
566 - name: stg_customers
567"#,
568 )
569 .unwrap();
570
571 let names = expand_yaml_names(&yaml_path);
572 assert_eq!(names, vec!["stg_orders", "stg_customers"]);
573 }
574
575 #[test]
576 fn test_expand_yaml_mixed() {
577 let tmp = tempfile::tempdir().unwrap();
578 let yaml_path = tmp.path().join("schema.yml");
579 fs::write(
580 &yaml_path,
581 r#"
582sources:
583 - name: raw
584 tables:
585 - name: orders
586models:
587 - name: stg_orders
588"#,
589 )
590 .unwrap();
591
592 let names = expand_yaml_names(&yaml_path);
593 assert_eq!(names, vec!["raw.orders", "stg_orders"]);
594 }
595
596 #[test]
597 fn test_expand_yaml_semantic_layer_types() {
598 let tmp = tempfile::tempdir().unwrap();
599 let yaml_path = tmp.path().join("semantic.yml");
600 fs::write(
601 &yaml_path,
602 r#"
603semantic_models:
604 - name: orders_sm
605 model: ref('orders')
606metrics:
607 - name: revenue
608 type: simple
609 type_params:
610 measure: total_revenue
611saved_queries:
612 - name: revenue_by_month
613"#,
614 )
615 .unwrap();
616
617 let names = expand_yaml_names(&yaml_path);
618 assert!(
619 names.contains(&"semantic_model.orders_sm".to_string()),
620 "missing semantic_model.orders_sm in {:?}",
621 names
622 );
623 assert!(
624 names.contains(&"metric.revenue".to_string()),
625 "missing metric.revenue in {:?}",
626 names
627 );
628 assert!(
629 names.contains(&"saved_query.revenue_by_month".to_string()),
630 "missing saved_query.revenue_by_month in {:?}",
631 names
632 );
633 }
634
635 #[test]
636 fn test_expand_yaml_file_not_found() {
637 let names = expand_yaml_names(Path::new("/nonexistent/schema.yml"));
638 assert!(names.is_empty());
639 }
640
641 #[test]
642 fn test_expand_yaml_empty_file() {
643 let tmp = tempfile::tempdir().unwrap();
644 let yaml_path = tmp.path().join("schema.yml");
645 fs::write(&yaml_path, "").unwrap();
646
647 let names = expand_yaml_names(&yaml_path);
648 assert!(names.is_empty());
649 }
650
651 #[test]
654 fn test_has_path_like_input_with_paths() {
655 assert!(has_path_like_input(&["models/foo.sql".into()]));
656 assert!(has_path_like_input(&[
657 "stg_orders".into(),
658 "models/bar.yml".into()
659 ]));
660 assert!(has_path_like_input(&["schema.yaml".into()]));
661 }
662
663 #[test]
664 fn test_has_path_like_input_model_names_only() {
665 assert!(!has_path_like_input(&["stg_orders".into()]));
666 assert!(!has_path_like_input(&[
667 "raw.orders".into(),
668 "customers".into()
669 ]));
670 }
671
672 #[test]
675 fn test_resolve_stdin_model_name() {
676 let tmp = tempfile::tempdir().unwrap();
677 let paths = make_resolved_paths(tmp.path());
678 let graph = LineageGraph::new();
679
680 let lines = vec!["stg_orders".to_string()];
681 let result = resolve_stdin_inputs(&lines, &graph, &paths, tmp.path(), tmp.path());
682 assert_eq!(result, vec!["stg_orders"]);
683 }
684
685 #[test]
686 fn test_resolve_stdin_ignores_non_dbt() {
687 let tmp = tempfile::tempdir().unwrap();
688 let paths = make_resolved_paths(tmp.path());
689 let graph = LineageGraph::new();
690
691 let lines = vec!["docs/README.md".to_string(), "seeds/data.csv".to_string()];
693 let result = resolve_stdin_inputs(&lines, &graph, &paths, tmp.path(), tmp.path());
694 assert!(result.is_empty());
695 }
696
697 #[test]
698 fn test_resolve_stdin_deduplicates() {
699 let tmp = tempfile::tempdir().unwrap();
700 let paths = make_resolved_paths(tmp.path());
701 let mut graph = LineageGraph::new();
702 let mut node = make_node("model.stg_orders", "stg_orders", NodeType::Model);
703 node.file_path = Some(PathBuf::from("models/stg_orders.sql"));
704 graph.add_node(node);
705
706 let models_dir = tmp.path().join("models");
708 fs::create_dir_all(&models_dir).unwrap();
709 let lines = vec![
710 "models/stg_orders.sql".to_string(),
711 "stg_orders".to_string(),
712 ];
713 let result = resolve_stdin_inputs(&lines, &graph, &paths, tmp.path(), tmp.path());
714 assert_eq!(result, vec!["stg_orders"]);
715 }
716
717 #[test]
718 fn test_resolve_stdin_ignores_root_files() {
719 let tmp = tempfile::tempdir().unwrap();
720 let paths = make_resolved_paths(tmp.path());
721 let graph = LineageGraph::new();
722
723 let lines = vec![
725 "README.md".to_string(),
726 "Cargo.toml".to_string(),
727 "stg_orders".to_string(),
728 ];
729 let result = resolve_stdin_inputs(&lines, &graph, &paths, tmp.path(), tmp.path());
730 assert_eq!(result, vec!["stg_orders"]);
731 }
732
733 #[test]
736 fn test_classify_and_resolve_sql() {
737 let tmp = tempfile::tempdir().unwrap();
738 let paths = make_resolved_paths(tmp.path());
739 let mut graph = LineageGraph::new();
740 let mut node = make_node("model.stg_orders", "stg_orders", NodeType::Model);
741 node.file_path = Some(PathBuf::from("models/staging/stg_orders.sql"));
742 graph.add_node(node);
743
744 let line = "models/staging/stg_orders.sql";
746 match classify_line(line, &paths, tmp.path()) {
747 InputLine::SqlFile(abs_path) => {
748 let label = resolve_sql_to_label(&abs_path, &graph, tmp.path());
749 assert_eq!(label, Some("stg_orders".to_string()));
750 }
751 other => panic!("Expected SqlFile, got {:?}", std::mem::discriminant(&other)),
752 }
753 }
754
755 #[test]
756 fn test_classify_and_resolve_yaml() {
757 let tmp = tempfile::tempdir().unwrap();
758 let models_dir = tmp.path().join("models");
759 fs::create_dir_all(&models_dir).unwrap();
760 fs::write(
761 models_dir.join("schema.yml"),
762 "sources:\n - name: raw\n tables:\n - name: orders\n",
763 )
764 .unwrap();
765
766 let paths = make_resolved_paths(tmp.path());
767
768 let line = "models/schema.yml";
769 match classify_line(line, &paths, tmp.path()) {
770 InputLine::YamlFile(abs_path) => {
771 let names = expand_yaml_names(&abs_path);
772 assert_eq!(names, vec!["raw.orders"]);
773 }
774 other => panic!(
775 "Expected YamlFile, got {:?}",
776 std::mem::discriminant(&other)
777 ),
778 }
779 }
780
781 #[test]
782 fn test_classify_and_resolve_mixed() {
783 let tmp = tempfile::tempdir().unwrap();
784 let models_dir = tmp.path().join("models");
785 fs::create_dir_all(models_dir.join("staging")).unwrap();
786 fs::write(
787 models_dir.join("schema.yml"),
788 "sources:\n - name: raw\n tables:\n - name: orders\n",
789 )
790 .unwrap();
791
792 let paths = make_resolved_paths(tmp.path());
793 let mut graph = LineageGraph::new();
794 let mut node = make_node("model.stg_orders", "stg_orders", NodeType::Model);
795 node.file_path = Some(PathBuf::from("models/staging/stg_orders.sql"));
796 graph.add_node(node);
797
798 let inputs = vec![
799 "models/staging/stg_orders.sql",
800 "models/schema.yml",
801 "raw.customers",
802 ".github/workflows/ci.yml",
803 "docs/README.md",
804 ];
805
806 let mut result = Vec::new();
807 for line in inputs {
808 match classify_line(line, &paths, tmp.path()) {
809 InputLine::SqlFile(abs) => {
810 if let Some(label) = resolve_sql_to_label(&abs, &graph, tmp.path()) {
811 result.push(label);
812 }
813 }
814 InputLine::YamlFile(abs) => {
815 result.extend(expand_yaml_names(&abs));
816 }
817 InputLine::ModelName(name) => result.push(name),
818 InputLine::Ignore => {}
819 }
820 }
821 assert_eq!(result, vec!["stg_orders", "raw.orders", "raw.customers"]);
822 }
823
824 #[test]
825 fn test_subdir_project_path_resolution() {
826 let tmp = tempfile::tempdir().unwrap();
828 let dbt_dir = tmp.path().join("dbt");
829 let models_dir = dbt_dir.join("models");
830 fs::create_dir_all(&models_dir).unwrap();
831
832 let paths = make_resolved_paths(&dbt_dir);
834
835 let mut graph = LineageGraph::new();
836 let mut node = make_node("model.stg_orders", "stg_orders", NodeType::Model);
837 node.file_path = Some(PathBuf::from("models/stg_orders.sql"));
839 graph.add_node(node);
840
841 let line = "dbt/models/stg_orders.sql";
843 match classify_line(line, &paths, tmp.path()) {
845 InputLine::SqlFile(abs_path) => {
846 let label = resolve_sql_to_label(&abs_path, &graph, &dbt_dir);
849 assert_eq!(label, Some("stg_orders".to_string()));
850 }
851 other => panic!("Expected SqlFile, got {:?}", std::mem::discriminant(&other)),
852 }
853 }
854
855 #[cfg(unix)]
860 #[test]
861 fn test_dev_null_is_not_fifo_or_file() {
862 use std::os::unix::fs::FileTypeExt;
863
864 let f = std::fs::File::open("/dev/null").unwrap();
865 let ft = f.metadata().unwrap().file_type();
866 assert!(!ft.is_fifo());
868 assert!(!ft.is_file());
869 }
870
871 #[cfg(unix)]
873 #[test]
874 fn test_regular_file_is_file() {
875 let tmp = tempfile::NamedTempFile::new().unwrap();
876 let ft = tmp.as_file().metadata().unwrap().file_type();
877 assert!(ft.is_file());
878 }
879}