Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion crates/sqlbuild-cli/src/argv/command_table.json
Original file line number Diff line number Diff line change
Expand Up @@ -2899,7 +2899,9 @@
"nargs": 0,
"const": true,
"default": false,
"help": "disable SQL syntax, binding, type inference, and semantic validation",
"help": {
"suppress": true
},
"group": 1
},
{
Expand Down
50 changes: 49 additions & 1 deletion crates/sqlbuild-cli/src/argv/tests/test_parse_cli_arguments.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::argv::models::{ArgvOutcome, Value};
use crate::argv::tests::helpers::{parse, strings};
use crate::argv::tests::test_types::{ExitTestCase, ParsedValueTestCase};
use crate::argv::tests::test_types::{ExitTestCase, HelpTextTestCase, ParsedValueTestCase};

#[test]
fn given_accepted_arguments_when_parsing_then_attributes_hold_the_parsed_values() {
Expand Down Expand Up @@ -29,6 +29,12 @@ fn given_accepted_arguments_when_parsing_then_attributes_hold_the_parsed_values(
attribute: "dbt_args",
expected_value: strings(&["--", "--no-color"]),
},
ParsedValueTestCase {
description: "clone still accepts the hidden --no-sql-analysis flag",
argv: &["clone", "--from", "prod", "--no-sql-analysis"],
attribute: "no_sql_validation",
expected_value: Value::Bool(true),
},
ParsedValueTestCase {
description: "a nested sub-command's dest is set",
argv: &["contract", "diff", "--from", "prod"],
Expand Down Expand Up @@ -100,3 +106,45 @@ fn given_help_version_or_rejected_arguments_when_parsing_then_exits_after_writin
);
}
}

#[test]
fn given_command_help_when_parsing_then_hidden_options_are_left_out() {
let test_cases = [
HelpTextTestCase {
description: "clone help omits the no-op SQL analysis flag",
argv: &["clone", "--help"],
expected_present: &["--from FROM_TARGET", "--select"],
expected_absent: &["--no-sql-analysis", "--no-sql-validation"],
},
HelpTextTestCase {
description: "build help keeps the SQL analysis flag",
argv: &["build", "--help"],
expected_present: &["--no-sql-analysis"],
expected_absent: &[],
},
];

for test_case in test_cases {
let outcome: ArgvOutcome = parse(test_case.argv);
let help: String = outcome
.writes
.iter()
.map(|(_, text)| text.as_str())
.collect();
let present: Vec<&str> = test_case
.expected_present
.iter()
.copied()
.filter(|fragment| help.contains(fragment))
.collect();
let absent: Vec<&str> = test_case
.expected_absent
.iter()
.copied()
.filter(|fragment| !help.contains(fragment))
.collect();
assert_eq!(outcome.parsed, Err(0), "{}", test_case.description);
assert_eq!(present, test_case.expected_present, "{help}");
assert_eq!(absent, test_case.expected_absent, "{help}");
}
}
8 changes: 8 additions & 0 deletions crates/sqlbuild-cli/src/argv/tests/test_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ pub(super) struct ExitTestCase {
pub(super) expected_last_write: &'static str,
}

/// A help request and fragments its text must and must not contain.
pub(super) struct HelpTextTestCase {
pub(super) description: &'static str,
pub(super) argv: &'static [&'static str],
pub(super) expected_present: &'static [&'static str],
pub(super) expected_absent: &'static [&'static str],
}

/// Text, width and indent for `textwrap.wrap`, and the expected lines.
pub(super) struct WrapTestCase {
pub(super) description: &'static str,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub fn render_clone_completion(
label: "TOTAL".to_owned(),
value: count(items, |_| true),
});
let elapsed: Option<String> = elapsed_seconds.map(|seconds| format!("({seconds:.2}s)"));
let elapsed: Option<String> = elapsed_seconds.map(|seconds| format!("{seconds:.2}s"));
let footer: String = format_summary_footer(style, &counts, elapsed.as_deref());
let warnings: i64 = count(items, |item| item.status == CloneStatusKind::Warning);
let failures: i64 = count(items, |item| item.status == CloneStatusKind::Failed);
Expand Down
4 changes: 2 additions & 2 deletions crates/sqlbuild-core/src/clone_text/tests/test_clone_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,12 @@ fn given_items_when_completing_then_reports_messages_counts_and_exit_code() {
expected_exit_code: 1,
},
CompletionTestCase {
description: "success shows the doubly parenthesised elapsed time",
description: "success shows the elapsed time in one pair of parentheses",
items: vec![item("orders", "copied", CloneStatusKind::Success)],
elapsed_seconds: Some(1.5),
use_color: false,
expected_text: "\n\u{2713} Completed successfully CLONED=0 COPIED=1 RECREATED_VIEWS=0 \
RECREATED_FUNCTIONS=0 PASS=1 WARN=0 FAIL=0 TOTAL=1 ((1.50s))\n",
RECREATED_FUNCTIONS=0 PASS=1 WARN=0 FAIL=0 TOTAL=1 (1.50s)\n",
expected_exit_code: 0,
},
CompletionTestCase {
Expand Down
2 changes: 2 additions & 0 deletions crates/sqlbuild-core/src/diagnostic_routing/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,5 @@ pub const LEVEL_INFO: i64 = 20;
pub const LEVEL_WARNING: i64 = 30;
pub const LEVEL_ERROR: i64 = 40;
pub const LEVEL_CRITICAL: i64 = 50;
/// Directories that hold installed packages, even inside a project (a project `.venv`).
pub const PACKAGE_DIRECTORIES: [&str; 2] = ["site-packages", "dist-packages"];
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//! `console_summary_line`: the one line a summarized console record shows.

/// The first line of `message`, without trailing whitespace.
#[must_use]
pub fn console_summary_line(message: &str) -> &str {
message.lines().next().unwrap_or_default().trim_end()
}
3 changes: 3 additions & 0 deletions crates/sqlbuild-core/src/diagnostic_routing/main/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
pub mod compute_log_admits;
pub mod console_summary_line;
pub mod diagnostic_severity;
pub mod internal_logger;
pub mod project_source_path;
pub mod route_admits;
pub mod sql_digest;
pub mod third_party_console_form;
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//! `project_source_path`: whether a logging call came from the project's own Python.

use std::path::{Component, Path};

use crate::diagnostic_routing::constants::PACKAGE_DIRECTORIES;

/// Whether `pathname` lies under one of `project_roots` and outside installed packages.
#[must_use]
pub fn project_source_path(pathname: &str, project_roots: &[String]) -> bool {
let path: &Path = Path::new(pathname);
project_roots
.iter()
.any(|root| !root.is_empty() && path.starts_with(root))
&& !path.components().any(|component| {
matches!(component, Component::Normal(name)
if PACKAGE_DIRECTORIES.iter().any(|package| name == *package))
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//! `third_party_console_form`: how a non-debug console shows a third-party record.

use crate::diagnostic_routing::constants::{LEVEL_ERROR, LEVEL_WARNING};
use crate::diagnostic_routing::models::ThirdPartyConsoleForm;

/// Errors in full, warnings as one line, and nothing below warning.
#[must_use]
pub fn third_party_console_form(level: i64) -> ThirdPartyConsoleForm {
if level >= LEVEL_ERROR {
ThirdPartyConsoleForm::Full
} else if level >= LEVEL_WARNING {
ThirdPartyConsoleForm::Summary
} else {
ThirdPartyConsoleForm::Hidden
}
}
11 changes: 11 additions & 0 deletions crates/sqlbuild-core/src/diagnostic_routing/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,14 @@ pub struct RouteOptions {
/// This destination receives SQL text, so it only accepts records that carry it.
pub include_sql_text: bool,
}

/// How a console that is not in debug mode shows one third-party record.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ThirdPartyConsoleForm {
/// The record stays off the console.
Hidden,
/// The first line of the message, without a traceback.
Summary,
/// The whole message and traceback.
Full,
}
104 changes: 102 additions & 2 deletions crates/sqlbuild-core/src/diagnostic_routing/tests/test_routing.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
//! Diagnostic routing decisions match the Python logging filter and compute log handler.

use crate::diagnostic_routing::main::compute_log_admits::compute_log_admits;
use crate::diagnostic_routing::main::console_summary_line::console_summary_line;
use crate::diagnostic_routing::main::diagnostic_severity::diagnostic_severity;
use crate::diagnostic_routing::main::internal_logger::internal_logger;
use crate::diagnostic_routing::main::project_source_path::project_source_path;
use crate::diagnostic_routing::main::route_admits::route_admits;
use crate::diagnostic_routing::main::sql_digest::sql_digest;
use crate::diagnostic_routing::models::{RecordFacts, RouteOptions};
use crate::diagnostic_routing::main::third_party_console_form::third_party_console_form;
use crate::diagnostic_routing::models::{RecordFacts, RouteOptions, ThirdPartyConsoleForm};
use crate::diagnostic_routing::tests::test_types::{
ComputeLogTestCase, DigestTestCase, InternalLoggerTestCase, RouteTestCase, SeverityTestCase,
ComputeLogTestCase, ConsoleFormTestCase, DigestTestCase, InternalLoggerTestCase,
ProjectSourceTestCase, RouteTestCase, SeverityTestCase, SummaryLineTestCase,
};

const EXTERNAL_DEBUG: RecordFacts = RecordFacts {
Expand Down Expand Up @@ -198,3 +202,99 @@ fn given_sql_when_digesting_then_prefixed_sha256_hex() {
);
}
}

#[test]
fn given_third_party_levels_when_choosing_console_form_then_only_warnings_and_errors_show() {
let test_cases = [
ConsoleFormTestCase {
description: "INFO stays off the console",
level: 20,
expected_form: ThirdPartyConsoleForm::Hidden,
},
ConsoleFormTestCase {
description: "WARNING shows one line",
level: 30,
expected_form: ThirdPartyConsoleForm::Summary,
},
ConsoleFormTestCase {
description: "ERROR shows in full",
level: 40,
expected_form: ThirdPartyConsoleForm::Full,
},
ConsoleFormTestCase {
description: "CRITICAL shows in full",
level: 50,
expected_form: ThirdPartyConsoleForm::Full,
},
];

for test_case in test_cases {
assert_eq!(
third_party_console_form(test_case.level),
test_case.expected_form,
"{}",
test_case.description
);
}
}

#[test]
fn given_messages_when_summarizing_then_first_line_remains() {
let test_cases = [
SummaryLineTestCase {
description: "multi-line message keeps its first line",
message: "token refresh failed \nTraceback (most recent call last):\n boom",
expected_line: "token refresh failed",
},
SummaryLineTestCase {
description: "empty message stays empty",
message: "",
expected_line: "",
},
];

for test_case in test_cases {
assert_eq!(
console_summary_line(test_case.message),
test_case.expected_line,
"{}",
test_case.description
);
}
}

#[test]
fn given_record_paths_when_classifying_then_only_project_files_outside_packages_are_project() {
let roots: Vec<String> = vec!["/work/orders_shop".to_owned()];
let test_cases = [
ProjectSourceTestCase {
description: "a project hook file is project code",
pathname: "/work/orders_shop/hooks/python/logs.py",
expected_project: true,
},
ProjectSourceTestCase {
description: "a package in a project virtual environment is a library",
pathname: "/work/orders_shop/.venv/lib/python3.12/site-packages/vendor/client.py",
expected_project: false,
},
ProjectSourceTestCase {
description: "a sibling directory sharing the prefix is outside the project",
pathname: "/work/orders_shop_tools/client.py",
expected_project: false,
},
ProjectSourceTestCase {
description: "the standard library is outside the project",
pathname: "/usr/lib/python3.12/logging/__init__.py",
expected_project: false,
},
];

for test_case in test_cases {
assert_eq!(
project_source_path(test_case.pathname, &roots),
test_case.expected_project,
"{}",
test_case.description
);
}
}
20 changes: 19 additions & 1 deletion crates/sqlbuild-core/src/diagnostic_routing/tests/test_types.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::diagnostic_routing::models::{RecordFacts, RouteOptions};
use crate::diagnostic_routing::models::{RecordFacts, RouteOptions, ThirdPartyConsoleForm};

pub(super) struct InternalLoggerTestCase {
pub(super) description: &'static str,
Expand Down Expand Up @@ -31,3 +31,21 @@ pub(super) struct DigestTestCase {
pub(super) sql: &'static str,
pub(super) expected_digest: &'static str,
}

pub(super) struct ConsoleFormTestCase {
pub(super) description: &'static str,
pub(super) level: i64,
pub(super) expected_form: ThirdPartyConsoleForm,
}

pub(super) struct SummaryLineTestCase {
pub(super) description: &'static str,
pub(super) message: &'static str,
pub(super) expected_line: &'static str,
}

pub(super) struct ProjectSourceTestCase {
pub(super) description: &'static str,
pub(super) pathname: &'static str,
pub(super) expected_project: bool,
}
5 changes: 4 additions & 1 deletion crates/sqlbuild-core/src/progress/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ pub(crate) const KIND_WIDTH: usize = 10;
pub(crate) const NESTED_PREFIX: &str = " ";
pub(crate) const UNKNOWN_RESOURCE_NAME: &str = "warehouse";
/// Messages that complete a planning phase rather than update it.
pub(crate) const PLANNING_COMPLETION_PREFIXES: [&str; 14] = [
pub(crate) const PLANNING_COMPLETION_PREFIXES: [&str; 15] = [
"Built ",
"Checked ",
"Compiled ",
Expand All @@ -91,11 +91,14 @@ pub(crate) const PLANNING_COMPLETION_PREFIXES: [&str; 14] = [
"Inspected ",
"Loaded ",
"Planned ",
"Prepared ",
"Recorded ",
"Refreshed ",
"Resolved ",
"Cloned ",
];
/// Messages that end a planning phase with a failure.
pub(crate) const PLANNING_FAILURE_PREFIXES: [&str; 1] = ["Failed "];
pub(crate) const ASSERTION_PREFIX: &str = "assertion ";
pub(crate) const PASS_OUTCOME: &str = "pass";
pub(crate) const ERROR_OUTCOME: &str = "error";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//! Whether a planning progress message ends its phase with a failure.

use crate::progress::constants::PLANNING_FAILURE_PREFIXES;

/// Failure messages start with `Failed `, as in `Failed runtime cursor watermark ...`.
#[must_use]
pub fn is_planning_failure_message(message: &str) -> bool {
PLANNING_FAILURE_PREFIXES
.iter()
.any(|prefix| message.starts_with(prefix))
}
1 change: 1 addition & 0 deletions crates/sqlbuild-core/src/progress/main/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ pub mod format_expectation_name;
pub mod format_inspection_record;
pub mod format_status_spinner_line;
pub mod is_planning_completion_message;
pub mod is_planning_failure_message;
pub mod machine_output_mode;
pub mod progress_terminal_duration;
Loading
Loading