use std::{
collections::{BTreeMap, BTreeSet},
fs,
path::{Path, PathBuf},
};
use syn::{
Expr, ExprCall, ExprMethodCall, ExprPath, ItemUse, Macro, UseTree, Visibility,
visit::{self, Visit},
};
const PRODUCTION_SOURCE_ROOTS: [&str; 3] = ["apps", "canisters", "crates"];
const NATIVE_REGISTRATION_CAPABILITIES: [&str; 5] = [
"register_after_completion",
"register_once",
"reconcile_after_completion",
"reconcile_once",
"reconcile_watchdog",
];
const NATIVE_REGISTRATION_ACTIONS: [&str; 7] = [
"cancel",
"ensure_scheduled",
"ensure_scheduled_immediately",
"reconcile_schedule",
"resume",
"suspend",
"unregister",
];
const RAW_TIMER_MECHANICS: [&str; 5] = [
"clear_timer",
"global_timer_set",
"set_global_timer",
"set_timer",
"set_timer_interval",
];
#[derive(Debug, Default, Eq, PartialEq)]
struct TimerSyntax {
has_timer_semantics: bool,
uses_native_provider: bool,
raw_provider_accesses: BTreeSet<String>,
native_registration_references: BTreeMap<String, usize>,
native_registration_calls: BTreeMap<String, usize>,
native_registration_actions: BTreeMap<String, usize>,
violations: Vec<String>,
}
#[derive(Default)]
struct TimerSyntaxVisitor {
analysis: TimerSyntax,
}
impl<'ast> Visit<'ast> for TimerSyntaxVisitor {
fn visit_ident(&mut self, identifier: &'ast proc_macro2::Ident) {
if timer_semantic_identifier(identifier.to_string().as_str()) {
self.analysis.has_timer_semantics = true;
}
visit::visit_ident(self, identifier);
}
fn visit_path(&mut self, path: &'ast syn::Path) {
let segments = path
.segments
.iter()
.map(|segment| segment.ident.to_string())
.collect::<Vec<_>>();
if segments.iter().any(|segment| segment == "ic_cdk_timers") {
self.analysis
.raw_provider_accesses
.insert("ic_cdk_timers".to_string());
}
if segments
.windows(2)
.any(|window| window == ["cdk", "timers"])
{
self.analysis
.raw_provider_accesses
.insert("cdk::timers".to_string());
}
for mechanic in segments
.iter()
.filter(|segment| RAW_TIMER_MECHANICS.contains(&segment.as_str()))
{
self.analysis
.raw_provider_accesses
.insert((*mechanic).clone());
}
if path
.segments
.first()
.is_some_and(|segment| segment.ident == "ic_timers")
{
self.analysis.uses_native_provider = true;
}
if path
.segments
.iter()
.any(|segment| timer_semantic_identifier(segment.ident.to_string().as_str()))
{
self.analysis.has_timer_semantics = true;
}
visit::visit_path(self, path);
}
fn visit_expr_call(&mut self, call: &'ast ExprCall) {
if let Expr::Path(function) = call.func.as_ref()
&& let Some(function_name) = function
.path
.segments
.last()
.map(|segment| segment.ident.to_string())
{
if NATIVE_REGISTRATION_CAPABILITIES.contains(&function_name.as_str()) {
self.analysis.has_timer_semantics = true;
*self
.analysis
.native_registration_calls
.entry(function_name)
.or_default() += 1;
} else if NATIVE_REGISTRATION_ACTIONS.contains(&function_name.as_str())
&& qualified_native_registration_action(&function.path)
{
self.analysis.has_timer_semantics = true;
*self
.analysis
.native_registration_actions
.entry(function_name)
.or_default() += 1;
}
}
visit::visit_expr_call(self, call);
}
fn visit_expr_method_call(&mut self, call: &'ast ExprMethodCall) {
let action = call.method.to_string();
if NATIVE_REGISTRATION_ACTIONS.contains(&action.as_str()) {
self.analysis.has_timer_semantics = true;
*self
.analysis
.native_registration_actions
.entry(action)
.or_default() += 1;
}
visit::visit_expr_method_call(self, call);
}
fn visit_expr_path(&mut self, expression: &'ast ExprPath) {
if let Some(capability) = expression
.path
.segments
.last()
.map(|segment| segment.ident.to_string())
&& NATIVE_REGISTRATION_CAPABILITIES.contains(&capability.as_str())
{
*self
.analysis
.native_registration_references
.entry(capability)
.or_default() += 1;
}
visit::visit_expr_path(self, expression);
}
fn visit_item_use(&mut self, item: &'ast ItemUse) {
let mut imports = Vec::new();
flatten_use_tree(&item.tree, &mut Vec::new(), &mut imports);
let public = !matches!(item.vis, Visibility::Inherited);
for import in imports {
let is_provider = import.path.first().is_some_and(|part| part == "ic_timers");
let is_raw_provider = import
.path
.first()
.is_some_and(|part| part == "ic_cdk_timers")
|| import
.path
.windows(2)
.any(|window| window == ["cdk", "timers"])
|| import
.path
.iter()
.any(|part| RAW_TIMER_MECHANICS.contains(&part.as_str()));
let is_authority = import.path.iter().any(|part| {
NATIVE_REGISTRATION_CAPABILITIES.contains(&part.as_str())
|| NATIVE_REGISTRATION_ACTIONS.contains(&part.as_str())
|| matches!(
part.as_str(),
"AfterCompletionRegistration"
| "OnceRegistration"
| "TimerApi"
| "TimerAuthorityWorkflow"
| "TimerIdentity"
| "TimerSchedule"
| "TimerSnapshot"
| "WatchdogRegistration"
)
});
let aliases_provider_module = is_provider
&& import
.path
.last()
.is_some_and(|part| matches!(part.as_str(), "ic_timers" | "self"));
if is_provider {
self.analysis.has_timer_semantics = true;
self.analysis.uses_native_provider = true;
}
if is_raw_provider {
self.analysis.has_timer_semantics = true;
self.analysis
.raw_provider_accesses
.insert(import.path.join("::"));
}
if import.rename.is_some() && (aliases_provider_module || is_authority) {
self.analysis.violations.push(format!(
"timer scheduling authority is renamed in `{}`",
import.path.join("::")
));
}
if public && (is_provider || is_authority) {
self.analysis.violations.push(format!(
"timer scheduling authority is publicly re-exported from `{}`",
import.path.join("::")
));
}
}
visit::visit_item_use(self, item);
}
fn visit_macro(&mut self, macro_invocation: &'ast Macro) {
visit_token_stream(¯o_invocation.tokens, &mut self.analysis);
visit::visit_macro(self, macro_invocation);
}
}
fn qualified_native_registration_action(path: &syn::Path) -> bool {
path.segments.len() > 1
&& (path
.segments
.first()
.is_some_and(|segment| segment.ident == "ic_timers")
|| path.segments.iter().any(|segment| {
matches!(
segment.ident.to_string().as_str(),
"AfterCompletionRegistration" | "OnceRegistration" | "WatchdogRegistration"
)
}))
}
struct UseImport {
path: Vec<String>,
rename: Option<String>,
}
fn analyze_timer_syntax(path: &str, source: &str) -> TimerSyntax {
let file = syn::parse_file(source).unwrap_or_else(|error| panic!("parse {path}: {error}"));
let mut visitor = TimerSyntaxVisitor::default();
visitor.visit_file(&file);
let mut analysis = visitor.analysis;
if analysis.native_registration_references != analysis.native_registration_calls {
analysis.violations.push(format!(
"native scheduling capability references are not direct calls: references={:?}, calls={:?}",
analysis.native_registration_references, analysis.native_registration_calls
));
}
analysis
}
fn flatten_use_tree(tree: &UseTree, prefix: &mut Vec<String>, imports: &mut Vec<UseImport>) {
match tree {
UseTree::Path(path) => {
prefix.push(path.ident.to_string());
flatten_use_tree(&path.tree, prefix, imports);
prefix.pop();
}
UseTree::Name(name) => {
let mut path = prefix.clone();
path.push(name.ident.to_string());
imports.push(UseImport { path, rename: None });
}
UseTree::Rename(rename) => {
let mut path = prefix.clone();
path.push(rename.ident.to_string());
imports.push(UseImport {
path,
rename: Some(rename.rename.to_string()),
});
}
UseTree::Glob(_) => imports.push(UseImport {
path: prefix.clone(),
rename: None,
}),
UseTree::Group(group) => {
for tree in &group.items {
flatten_use_tree(tree, prefix, imports);
}
}
}
}
fn timer_semantic_identifier(identifier: &str) -> bool {
let lowercase = identifier.to_ascii_lowercase();
lowercase.contains("async_job_recovery")
|| identifier.starts_with("AsyncJob")
|| matches!(
identifier,
"AfterCompletionRegistration"
| "CanisterTimerStatus"
| "CoreAsyncJobRecovery"
| "OnceRegistration"
| "TimerApi"
| "TimerAuthorityWorkflow"
| "TimerExecutionOutcome"
| "TimerIdentity"
| "TimerRegistrationStatus"
| "TimerSchedule"
| "TimerSnapshot"
| "WatchdogRegistration"
| "ic_cdk_timers"
| "ic_timers"
)
|| NATIVE_REGISTRATION_CAPABILITIES.contains(&identifier)
|| RAW_TIMER_MECHANICS.contains(&identifier)
}
fn visit_token_stream(tokens: &proc_macro2::TokenStream, analysis: &mut TimerSyntax) {
for token in tokens.clone() {
match token {
proc_macro2::TokenTree::Group(group) => visit_token_stream(&group.stream(), analysis),
proc_macro2::TokenTree::Ident(identifier) => {
let identifier = identifier.to_string();
if timer_semantic_identifier(identifier.as_str()) {
analysis.has_timer_semantics = true;
}
if NATIVE_REGISTRATION_CAPABILITIES.contains(&identifier.as_str()) {
*analysis
.native_registration_references
.entry(identifier.clone())
.or_default() += 1;
*analysis
.native_registration_calls
.entry(identifier.clone())
.or_default() += 1;
}
if NATIVE_REGISTRATION_ACTIONS.contains(&identifier.as_str()) {
*analysis
.native_registration_actions
.entry(identifier.clone())
.or_default() += 1;
}
if identifier == "ic_cdk_timers" {
analysis.raw_provider_accesses.insert(identifier.clone());
}
if RAW_TIMER_MECHANICS.contains(&identifier.as_str()) {
analysis.raw_provider_accesses.insert(identifier);
}
}
_ => {}
}
}
}
#[test]
fn timer_registration_custody_is_closed() {
let root = workspace_root();
let mut observed_call_owners = BTreeSet::new();
let mut observed_action_owners = BTreeSet::new();
for source_root in PRODUCTION_SOURCE_ROOTS {
collect_rust_sources(&root.join(source_root), &root, &mut |path, source| {
if excluded_test_source(path) {
return;
}
let syntax = analyze_timer_syntax(path, source);
assert!(
syntax.violations.is_empty(),
"{path} contains disguised or exported scheduling authority: {:?}",
syntax.violations
);
if !syntax.native_registration_calls.is_empty() {
observed_call_owners.insert(path.to_string());
}
if !syntax.native_registration_actions.is_empty() {
observed_action_owners.insert(path.to_string());
}
});
}
assert_eq!(
observed_call_owners,
native_registration_owners(),
"native timer registration custody changed"
);
assert_eq!(
observed_action_owners,
native_registration_owners(),
"native timer registration actions changed"
);
}
#[test]
fn semantic_inventory_ignores_comments_and_string_literals() {
let syntax = analyze_timer_syntax(
"synthetic.rs",
r#"
const NOTE: &str = "ic_timers::register_once";
// TimerApi::set was removed.
fn ordinary_business_logic() {}
"#,
);
assert_eq!(syntax, TimerSyntax::default());
}
#[test]
fn semantic_inventory_rejects_aliased_provider_authority() {
let syntax = analyze_timer_syntax(
"synthetic.rs",
"use ic_timers::register_once as schedule_later;",
);
assert!(syntax.has_timer_semantics);
assert_eq!(syntax.violations.len(), 1);
}
#[test]
fn semantic_inventory_rejects_value_aliased_provider_authority() {
let syntax = analyze_timer_syntax(
"synthetic.rs",
"fn alias() { let schedule = ic_timers::register_once; schedule(); }",
);
assert!(syntax.has_timer_semantics);
assert_eq!(syntax.violations.len(), 1);
}
#[test]
fn semantic_inventory_observes_unclassified_and_duplicate_native_calls() {
let syntax = analyze_timer_syntax(
"synthetic.rs",
r"
fn duplicate_custody() {
ic_timers::register_once(first());
ic_timers::register_once(second());
}
",
);
assert!(syntax.has_timer_semantics);
assert_eq!(syntax.native_registration_calls["register_once"], 2);
}
#[test]
fn semantic_inventory_observes_native_registration_method_actions() {
let syntax = analyze_timer_syntax(
"synthetic.rs",
r"
fn mutate(first: &Registration, second: &Registration) {
first.ensure_scheduled(schedule());
first.ensure_scheduled_immediately();
first.cancel();
second.cancel();
second.unregister();
}
",
);
assert!(syntax.has_timer_semantics);
assert_eq!(syntax.native_registration_actions["ensure_scheduled"], 1);
assert_eq!(
syntax.native_registration_actions["ensure_scheduled_immediately"],
1
);
assert_eq!(syntax.native_registration_actions["cancel"], 2);
assert_eq!(syntax.native_registration_actions["unregister"], 1);
}
#[test]
fn semantic_inventory_rejects_aliased_native_registration_actions() {
let syntax = analyze_timer_syntax(
"synthetic.rs",
"use ic_timers::OnceRegistration::cancel as cancel_later;",
);
assert!(syntax.has_timer_semantics);
assert_eq!(syntax.violations.len(), 1);
}
#[test]
fn semantic_inventory_counts_macro_generated_native_calls() {
let syntax = analyze_timer_syntax(
"synthetic.rs",
r"
macro_rules! schedule {
() => { ic_timers::register_once(identity(), lifetime(), callback()) };
}
",
);
assert!(syntax.has_timer_semantics);
assert_eq!(syntax.native_registration_calls["register_once"], 1);
}
#[test]
fn semantic_inventory_detects_raw_timer_mechanics() {
let syntax = analyze_timer_syntax(
"synthetic.rs",
"fn bypass() { ic_cdk::api::set_global_timer(1); }",
);
assert!(syntax.has_timer_semantics);
assert!(syntax.raw_provider_accesses.contains("set_global_timer"));
}
#[test]
fn semantic_inventory_detects_aliased_raw_provider_imports() {
let syntax = analyze_timer_syntax("synthetic.rs", "use ic_cdk_timers as raw;");
assert!(syntax.has_timer_semantics);
assert!(syntax.raw_provider_accesses.contains("ic_cdk_timers"));
}
#[test]
fn timer_provider_graph_and_manifest_consumers_are_closed() {
let root = workspace_root();
let lock = read_source(&root, "Cargo.lock");
assert_eq!(locked_package_versions(&lock, "ic-timers"), ["0.7.0"]);
assert_eq!(locked_package_versions(&lock, "ic-cdk-timers"), ["1.0.0"]);
let workspace_manifest = read_source(&root, "Cargo.toml");
let workspace_dependencies = workspace_dependencies(&workspace_manifest);
assert_eq!(
dependency_version(&workspace_dependencies, "ic-timers"),
"=0.7.0"
);
let icydb_version = dependency_version(&workspace_dependencies, "icydb")
.strip_prefix('=')
.filter(|version| !version.is_empty())
.expect("IcyDB must use an exact workspace version pin");
assert_eq!(locked_package_versions(&lock, "icydb"), [icydb_version]);
assert_eq!(
workspace_dependencies["icydb"]
.as_table()
.and_then(|dependency| dependency.get("default-features"))
.and_then(toml::Value::as_bool),
Some(false)
);
assert!(!workspace_dependencies.contains_key("icydb-model"));
assert!(!workspace_dependencies.contains_key("ic-cdk-timers"));
let mut direct_icydb_model_consumers = BTreeSet::new();
let mut timer_consumers = BTreeSet::from(["Cargo.toml".to_string()]);
let mut raw_provider_consumers = BTreeSet::new();
for source_root in PRODUCTION_SOURCE_ROOTS {
collect_named_files(
&root.join(source_root),
&root,
"Cargo.toml",
&mut |path, manifest| {
let dependencies = direct_dependency_names(manifest);
if dependencies.contains("ic-timers") {
timer_consumers.insert(path.to_string());
}
if dependencies.contains("ic-cdk-timers") {
raw_provider_consumers.insert(path.to_string());
}
if dependencies.contains("icydb-model") {
direct_icydb_model_consumers.insert(path.to_string());
}
},
);
}
assert!(direct_icydb_model_consumers.is_empty());
assert_eq!(timer_consumers, expected_timer_manifest_consumers());
assert!(raw_provider_consumers.is_empty());
}
#[test]
fn maintained_runtime_docs_do_not_advertise_the_removed_timer_facade() {
let root = workspace_root();
for path in [
"crates/canic/README.md",
"docs/features/runtime/README.md",
"docs/features/runtime/native-timers.md",
] {
let source = read_source(&root, path);
for forbidden in [
"TimerApi::cancel",
"TimerApi::set",
"canic::timer!(",
"canic::timer_interval!(",
"use canic::api::timer",
] {
assert!(
!source.contains(forbidden),
"maintained runtime document {path} advertises removed facade `{forbidden}`"
);
}
}
}
#[test]
fn direct_raw_timer_provider_access_is_absent_from_production() {
let root = workspace_root();
let mut violations = Vec::new();
for source_root in PRODUCTION_SOURCE_ROOTS {
collect_rust_sources(&root.join(source_root), &root, &mut |path, source| {
if excluded_test_source(path) {
return;
}
for forbidden in analyze_timer_syntax(path, source).raw_provider_accesses {
violations.push(format!("{path}: {forbidden}"));
}
});
}
assert!(
violations.is_empty(),
"production code bypasses the shared timer provider: {violations:?}"
);
}
#[test]
fn pool_and_snapshot_paths_use_exact_native_owners() {
let root = workspace_root();
let timer = read_source(&root, "crates/canic-core/src/workflow/runtime/timer/mod.rs");
let timer_api = read_source(&root, "crates/canic-core/src/api/timer.rs");
let pool = read_source(
&root,
"crates/canic-control-plane/src/workflow/canister_pool/mod.rs",
);
let lifecycle = read_source(&root, "crates/canic-control-plane/src/api/lifecycle.rs");
let coordinator = read_source(
&root,
"crates/canic-control-plane/src/api/fleet_coordinator.rs",
);
let authority = read_source(
&root,
"crates/canic-core/src/workflow/runtime/authority_restore.rs",
);
for forbidden in [
"static CLAIMS",
"enum TimerClaim",
"register_snapshot_resume_participant",
"register_async_job_recovery_participant",
] {
assert!(!timer.contains(forbidden));
assert!(!timer_api.contains(forbidden));
}
for required in [
"static MAINTENANCE_TIMER: RefCell<Option<AfterCompletionRegistration>>",
"static RECOVERY_WATCHDOG: RefCell<Option<WatchdogRegistration>>",
"reconcile_after_completion(",
"reconcile_watchdog(",
] {
assert!(
pool.contains(required),
"Root pool custody lacks `{required}`"
);
}
assert!(
lifecycle.contains("canister_pool::suspend_for_authority_snapshot()")
&& lifecycle.contains("canister_pool::resume_after_authority_snapshot()")
&& lifecycle.contains("AuthorityRestoreApi::prepare_root_snapshot(")
&& lifecycle.contains("AuthorityRestoreApi::resume_root_snapshot("),
"Root snapshot lifecycle must invoke its exact timer owner"
);
assert!(
coordinator.contains("AuthorityRestoreApi::prepare_coordinator_snapshot(")
&& coordinator.contains("AuthorityRestoreApi::resume_coordinator_snapshot("),
"Coordinator snapshot lifecycle must not link Root timer owners"
);
for required in [
"TimerAuthorityWorkflow::suspend_root",
"TimerAuthorityWorkflow::suspend_coordinator",
"TimerAuthorityWorkflow::resume_root",
"TimerAuthorityWorkflow::resume_coordinator",
] {
assert!(
authority.contains(required),
"authority restore lacks exact role path `{required}`"
);
}
}
#[test]
fn recovery_takeovers_recheck_each_owners_authoritative_domain_demand() {
let root = workspace_root();
let cases = [
(
"root issuer renewal",
"crates/canic-core/src/workflow/runtime/auth/renewal.rs",
[
"AsyncJobWorkflow::has_expired_attempt(owner, now_ns)",
"AuthOps::has_enabled_root_issuer_renewal_templates()",
"ConfigOps::delegated_tokens_config()",
"AsyncJobWorkflow::claim_expired(owner, now_ns)",
"Self::run_scheduled().await",
],
),
(
"automatic cycle top-up",
"crates/canic-core/src/workflow/runtime/cycles/mod.rs",
[
"AsyncJobWorkflow::has_expired_attempt(owner, now_ns)",
"Self::automatic_topup_config()",
"AsyncJobWorkflow::claim_expired(owner, now_ns)",
"Self::run_attempt(attempt).await",
"attempt.operation_id()",
],
),
(
"placement receipt acknowledgement",
"crates/canic-core/src/workflow/placement/acknowledgement.rs",
[
"AsyncJobWorkflow::has_expired_attempt(owner, now_ns)",
"ReceiptBackedIntentOps::has_placement_acknowledgements()",
"AsyncJobWorkflow::claim_expired(owner, now_ns)",
"Self::run_scheduled().await",
"let operation_id = intent.operation_id;",
],
),
];
for (owner, path, required) in cases {
let source = read_source(&root, path);
for fragment in required {
assert!(
source.contains(fragment),
"{owner} recovery lost authoritative binding `{fragment}`"
);
}
}
let pool = read_source(
&root,
"crates/canic-control-plane/src/workflow/canister_pool/mod.rs",
);
for required in [
"AsyncJobRecoveryOps::expired_deadline(AsyncJobOwner::CanisterPoolMaintenance, now_ns)",
"dispatch_async_job_recovery()",
"finish_maintenance_timer(attempt, maintain_once_inner().await)",
"CanisterPoolOps::pending_creation()",
"CanisterPoolOps::pending_reset_canisters()",
"CanisterPoolOps::ready_count()",
] {
assert!(
pool.contains(required),
"pool recovery lost authoritative binding `{required}`"
);
}
}
fn native_registration_owners() -> BTreeSet<String> {
[
"canisters/test/runtime_probe/src/lib.rs",
"crates/canic-control-plane/src/workflow/canister_pool/mod.rs",
"crates/canic-core/src/workflow/metrics/publication/timer/mod.rs",
"crates/canic-core/src/workflow/placement/acknowledgement.rs",
"crates/canic-core/src/workflow/runtime/auth/renewal.rs",
"crates/canic-core/src/workflow/runtime/cycles/mod.rs",
"crates/canic-core/src/workflow/runtime/intent.rs",
"crates/canic-core/src/workflow/runtime/log.rs",
"crates/canic-core/src/workflow/runtime/timer/mod.rs",
]
.into_iter()
.map(str::to_string)
.collect()
}
fn expected_timer_manifest_consumers() -> BTreeSet<String> {
[
"Cargo.toml",
"apps/test/test/Cargo.toml",
"canisters/test/canic_icydb_lifecycle_probe/Cargo.toml",
"canisters/test/runtime_probe/Cargo.toml",
"crates/canic-control-plane/Cargo.toml",
"crates/canic-core/Cargo.toml",
"crates/canic-tests/Cargo.toml",
]
.into_iter()
.map(str::to_string)
.collect()
}
fn locked_package_versions(lock: &str, wanted: &str) -> Vec<String> {
let lock: toml::Value = toml::from_str(lock).expect("Cargo.lock must be valid TOML");
lock.get("package")
.and_then(toml::Value::as_array)
.expect("Cargo.lock must contain package records")
.iter()
.filter(|package| package.get("name").and_then(toml::Value::as_str) == Some(wanted))
.map(|package| {
package
.get("version")
.and_then(toml::Value::as_str)
.unwrap_or_else(|| panic!("locked package {wanted} must have a version"))
.to_string()
})
.collect()
}
fn workspace_dependencies(manifest: &str) -> toml::Table {
let manifest: toml::Value =
toml::from_str(manifest).expect("workspace manifest must be valid TOML");
manifest
.get("workspace")
.and_then(toml::Value::as_table)
.and_then(|workspace| workspace.get("dependencies"))
.and_then(toml::Value::as_table)
.expect("workspace manifest must declare workspace dependencies")
.clone()
}
fn dependency_version<'a>(dependencies: &'a toml::Table, name: &str) -> &'a str {
let dependency = dependencies
.get(name)
.unwrap_or_else(|| panic!("workspace dependency {name} must be declared"));
dependency
.as_str()
.or_else(|| {
dependency
.as_table()
.and_then(|dependency| dependency.get("version"))
.and_then(toml::Value::as_str)
})
.unwrap_or_else(|| panic!("workspace dependency {name} must declare a version"))
}
fn direct_dependency_names(manifest: &str) -> BTreeSet<String> {
let manifest: toml::Value =
toml::from_str(manifest).expect("production manifest must be valid TOML");
let mut dependencies = BTreeSet::new();
collect_direct_dependency_names(&manifest, &mut dependencies);
dependencies
}
fn collect_direct_dependency_names(value: &toml::Value, dependencies: &mut BTreeSet<String>) {
let Some(table) = value.as_table() else {
return;
};
for (name, value) in table {
if matches!(
name.as_str(),
"dependencies" | "dev-dependencies" | "build-dependencies"
) {
if let Some(dependency_table) = value.as_table() {
dependencies.extend(dependency_table.keys().cloned());
}
} else {
collect_direct_dependency_names(value, dependencies);
}
}
}
fn collect_rust_sources(directory: &Path, root: &Path, visit: &mut impl FnMut(&str, &str)) {
collect_named_files(directory, root, "rs", visit);
}
fn collect_named_files(
directory: &Path,
root: &Path,
name_or_extension: &str,
visit: &mut impl FnMut(&str, &str),
) {
let mut entries = fs::read_dir(directory)
.unwrap_or_else(|err| panic!("read {}: {err}", directory.display()))
.collect::<Result<Vec<_>, _>>()
.unwrap_or_else(|err| panic!("read entry below {}: {err}", directory.display()));
entries.sort_by_key(std::fs::DirEntry::path);
for entry in entries {
let path = entry.path();
if path.is_dir() {
collect_named_files(&path, root, name_or_extension, visit);
continue;
}
let matches = path
.file_name()
.is_some_and(|name| name == name_or_extension)
|| path
.extension()
.is_some_and(|extension| extension == name_or_extension);
if !matches {
continue;
}
let relative = path
.strip_prefix(root)
.unwrap_or_else(|err| panic!("relativize {}: {err}", path.display()))
.to_string_lossy()
.replace('\\', "/");
let source = fs::read_to_string(&path)
.unwrap_or_else(|err| panic!("read {}: {err}", path.display()));
visit(&relative, &source);
}
}
fn excluded_test_source(path: &str) -> bool {
path.starts_with("crates/canic-testing-internal/")
|| path.contains("/tests/")
|| path.ends_with("/tests.rs")
|| path.ends_with("/test_support.rs")
}
fn read_source(root: &Path, relative: &str) -> String {
fs::read_to_string(root.join(relative)).unwrap_or_else(|err| panic!("read {relative}: {err}"))
}
fn workspace_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)
.map(PathBuf::from)
.expect("workspace root")
}