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
11 changes: 11 additions & 0 deletions .config/nextest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
[store]
dir = "target/nextest"

[test-groups]
nested-cargo = { max-threads = 12 }

[profile.default]
# Fail fast: stop after the first test failure during local development.
fail-fast = true
Expand All @@ -20,3 +23,11 @@ status-level = "slow"
final-status-level = "slow"
slow-timeout = "30s"
leak-timeout = "2s"

[[profile.default.overrides]]
filter = 'binary_id(incan::integration_tests) | binary_id(incan::cli_integration) | binary_id(incan::std_encoding_algorithm_modules) | binary_id(incan::generated_rust_artifact_tests) | binary_id(incan::generated_rust_callability_artifact_tests) | binary_id(incan::generated_rust_native_consumer_tests)'
test-group = 'nested-cargo'

[[profile.ci.overrides]]
filter = 'binary_id(incan::integration_tests) | binary_id(incan::cli_integration) | binary_id(incan::std_encoding_algorithm_modules) | binary_id(incan::generated_rust_artifact_tests) | binary_id(incan::generated_rust_callability_artifact_tests) | binary_id(incan::generated_rust_native_consumer_tests)'
test-group = 'nested-cargo'
12 changes: 7 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ TEST_VERBOSE ?= 0

ifeq ($(strip $(NEXTEST)),)
ifeq ($(TEST_VERBOSE),1)
TEST_CMD = cargo test --all --verbose
TEST_CMD = cargo test --all --features lsp --verbose
else
TEST_CMD = cargo test --all
TEST_CMD = cargo test --all --features lsp
endif
else
TEST_CMD = cargo nextest run --all --status-level all
ifeq ($(TEST_VERBOSE),1)
TEST_CMD = cargo nextest run --all --features lsp --status-level all
else
TEST_CMD = cargo nextest run --all --features lsp --status-level slow --final-status-level slow
endif
endif

# After `make build` / `make build-fast`, symlink ~/.cargo/bin/incan → target/debug/incan so `incan` on PATH (IDE run,
Expand Down Expand Up @@ -202,7 +206,6 @@ pre-commit-full-gate:
t2=$$(date +%s); \
echo "\033[1mRunning tests...\033[0m"; \
$(TEST_CMD); \
cargo test --features lsp unchecked_lookup_hover --lib; \
echo "\033[32mDONE\033[0m"; \
t3=$$(date +%s); \
echo "\033[1mRunning clippy...\033[0m"; \
Expand Down Expand Up @@ -322,7 +325,6 @@ smoke-test-benchmarks-incan:
.PHONY: smoke-test-core
smoke-test-core:
@$(MAKE) smoke-test-release
@$(MAKE) test-rust-inspect
@$(MAKE) smoke-test-canary
@$(MAKE) smoke-test-web-example
@$(MAKE) smoke-test-nested-project-example
Expand Down
5 changes: 3 additions & 2 deletions src/backend/project/cargo_toml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,10 +251,11 @@ impl ProjectGenerator {
};

// ---- Build bin/lib target ----
let target_name = self.cargo_target_name();
let (bin, lib) = if self.is_binary {
(
vec![BinTarget {
name: self.name.clone(),
name: target_name,
path: "src/main.rs".into(),
}],
None,
Expand All @@ -263,7 +264,7 @@ impl ProjectGenerator {
(
vec![],
Some(LibTarget {
name: self.name.clone(),
name: target_name,
path: "src/lib.rs".into(),
}),
)
Expand Down
75 changes: 75 additions & 0 deletions src/backend/project/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ use std::path::{Path, PathBuf};

use crate::manifest::DependencySpec;
use incan_core::lang::rust_keywords;
use sha2::{Digest as _, Sha256};

const MOD_INSERT_MARKER: &str = "// __INCAN_INSERT_MODS__";
pub(crate) const GENERATED_CARGO_TARGET_DIR_ENV: &str = "INCAN_GENERATED_CARGO_TARGET_DIR";

// ============================================================================
// RFC 023: Stdlib module naming
Expand Down Expand Up @@ -151,6 +153,79 @@ impl ProjectGenerator {
self.run_profile = profile;
}

/// Resolve the optional generated-project Cargo target override.
///
/// This is primarily used by integration tests and smoke gates that compile many generated Rust projects from one
/// parent workspace. It lets those projects share dependency artifacts while keeping ordinary user invocations on
/// the parent-scoped default target directory.
pub(super) fn generated_cargo_target_dir_override() -> Option<PathBuf> {
let raw = std::env::var_os(GENERATED_CARGO_TARGET_DIR_ENV)?;
let raw = PathBuf::from(raw);
if raw.as_os_str().is_empty() {
return None;
}
Some(Self::resolve_target_dir(raw))
}

pub(super) fn resolve_target_dir(target_dir: PathBuf) -> PathBuf {
if target_dir.is_absolute() {
target_dir
} else if let Ok(cwd) = std::env::current_dir() {
cwd.join(target_dir)
} else {
target_dir
}
}

/// Cargo target name used for the generated binary or library target.
///
/// When a caller opts into a broad shared target directory, multiple unrelated generated projects can have the same
/// user-facing project name (`main`, `consumer`, etc.). Cargo writes root binaries and libraries at
/// `target/<profile>/<target-name>`, so shared target dirs need a unique target name to avoid stale binary reuse
/// and parallel build collisions. Library target names stay stable because native Rust consumers import them as
/// crate names from generated library artifacts.
pub(super) fn cargo_target_name(&self) -> String {
if self.is_binary && Self::generated_cargo_target_dir_override().is_some() {
Self::shared_target_safe_name(&self.name, &self.output_dir)
} else {
self.name.clone()
}
}

pub(super) fn shared_target_safe_name(name: &str, output_dir: &Path) -> String {
let mut normalized = name
.chars()
.map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' })
.collect::<String>();
if normalized.is_empty() {
normalized.push_str("incan_project");
}
if !normalized
.as_bytes()
.first()
.is_some_and(|byte| byte.is_ascii_alphabetic() || *byte == b'_')
{
normalized.insert(0, '_');
}

let absolute_output_dir = if output_dir.is_absolute() {
output_dir.to_path_buf()
} else if let Ok(cwd) = std::env::current_dir() {
cwd.join(output_dir)
} else {
output_dir.to_path_buf()
};

let mut hasher = Sha256::new();
hasher.update(name.as_bytes());
hasher.update(b"\0");
hasher.update(absolute_output_dir.to_string_lossy().as_bytes());
let digest_bytes = hasher.finalize();
let digest = hex::encode(&digest_bytes[..8]);

format!("{normalized}_{digest}")
}

/// Ensure the generated `src/` directory exists.
fn ensure_generated_src_dir(&self) -> io::Result<PathBuf> {
let src_dir = self.output_dir.join("src");
Expand Down
40 changes: 31 additions & 9 deletions src/backend/project/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,14 @@ impl ProjectGenerator {
/// tests, and benchmark checks. Sharing a parent-scoped target dir lets those generated crates reuse compiled
/// dependencies.
fn cargo_target_dir(&self) -> PathBuf {
if let Some(target_dir) = Self::generated_cargo_target_dir_override() {
return target_dir;
}

let base_dir = self.output_dir.parent().unwrap_or(self.output_dir.as_path());
let target_dir = base_dir.join(".cargo-target");

if target_dir.is_absolute() {
target_dir
} else if let Ok(cwd) = std::env::current_dir() {
cwd.join(target_dir)
} else {
target_dir
}
Self::resolve_target_dir(target_dir)
}

/// Build the project using cargo.
Expand Down Expand Up @@ -167,14 +165,14 @@ impl ProjectGenerator {

/// Get the path to the built binary.
pub fn binary_path(&self) -> PathBuf {
self.cargo_target_dir().join("release").join(&self.name)
self.cargo_target_dir().join("release").join(self.cargo_target_name())
}

/// Get the path to the binary produced for `incan run`.
pub fn run_binary_path(&self) -> PathBuf {
self.cargo_target_dir()
.join(self.run_profile_binary_dir())
.join(&self.name)
.join(self.cargo_target_name())
}
}

Expand Down Expand Up @@ -258,4 +256,28 @@ mod tests {
);
Ok(())
}

#[test]
fn shared_target_safe_name_distinguishes_same_project_name_by_output_dir() -> Result<(), Box<dyn std::error::Error>>
{
let tmp = tempfile::tempdir()?;
let first = ProjectGenerator::shared_target_safe_name("demo-app", &tmp.path().join("one"));
let second = ProjectGenerator::shared_target_safe_name("demo-app", &tmp.path().join("two"));

assert_ne!(first, second);
assert!(first.starts_with("demo_app_"), "unexpected target name: {first}");
assert!(
first.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '_'),
"target name should be Rust-identifier safe: {first}"
);
Ok(())
}

#[test]
fn relative_target_dirs_resolve_against_current_working_dir() -> Result<(), Box<dyn std::error::Error>> {
let cwd = std::env::current_dir()?;
let target_dir = ProjectGenerator::resolve_target_dir(PathBuf::from("target/shared-generated"));
assert_eq!(target_dir, cwd.join("target/shared-generated"));
Ok(())
}
}
15 changes: 15 additions & 0 deletions src/cli/test_runner/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -435,8 +435,23 @@ fn normalize_runner_assert_statements(ast: &mut Program) {
/// By default this reuses the project's main `target/` so existing dependency artifacts are shared across regular
/// builds and `incan test` runs for better DX.
///
/// Set `INCAN_TEST_SHARED_TARGET_DIR` to force all generated test harnesses into a caller-provided target directory.
/// This is primarily useful for integration tests that create many throwaway project roots but should still reuse the
/// same compiled harness dependencies.
///
/// Set `INCAN_TEST_ISOLATED_TARGET_DIR` to one of `1|true|yes|on` to use `target/incan_test_runner` instead.
fn shared_cargo_target_dir(project_root: &Path) -> PathBuf {
if let Ok(shared_target_dir) = std::env::var("INCAN_TEST_SHARED_TARGET_DIR") {
let shared_target_dir = PathBuf::from(shared_target_dir);
if shared_target_dir.is_absolute() {
return shared_target_dir;
}
if let Ok(cwd) = std::env::current_dir() {
return cwd.join(shared_target_dir);
}
return shared_target_dir;
}

let absolute_project_root = if project_root.is_absolute() {
project_root.to_path_buf()
} else if let Ok(cwd) = std::env::current_dir() {
Expand Down
6 changes: 3 additions & 3 deletions src/lsp/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1054,7 +1054,7 @@ mod lsp_api_metadata_preview_tests {
}

#[test]
fn checked_api_previews_use_callable_rebound_function_signature() -> Result<(), String> {
fn checked_api_previews_preserve_source_signature_for_callable_rebound() -> Result<(), String> {
let source = r#"
pub def endpoint() -> str:
return "raw"
Expand Down Expand Up @@ -1089,8 +1089,8 @@ pub def endpoint() -> str:
.ok_or_else(|| "expected checked function preview".to_string())?;

assert!(
preview.markdown.contains("pub def endpoint(id: int) -> bool"),
"expected rebound callable signature in LSP preview, got:\n{}",
preview.markdown.contains("pub def endpoint() -> str"),
"expected source declaration signature in LSP preview, got:\n{}",
preview.markdown
);

Expand Down
Loading
Loading