use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use bynk_emit::project::{self, CompileOptions, ProjectPathsError, try_read_project_paths_with};
use bynk_fmt::{FormatOptions, IndentStyle, format_source};
use bynk_render::LabelHome;
pub mod coverage;
pub mod dev_run;
pub mod discovery;
pub mod output;
pub mod probe;
pub mod remote;
pub mod schema_lock;
pub mod test_json;
pub mod test_runner;
pub use output::{write_document, write_output};
pub fn project_options(input: &Path) -> Result<CompileOptions, discovery::DiscoveryError> {
if input.join("bynk.toml").exists() || input.join("src").is_dir() {
let overlay = manifest_overlay(input);
let paths = try_read_project_paths_with(input, &overlay)
.unwrap_or_else(|_| project::ProjectPaths::conventional(input));
let workers = project::try_read_workers_config_with(input, &overlay).unwrap_or_default();
Ok(options_for_split(input, paths)?
.compatibility_flags(workers.compatibility_flags)
.asset_files(discovery::list_asset_folder(input)?))
} else {
let sources = discovery::read_bynk_tree_single(input)?;
Ok(CompileOptions::single(input.to_path_buf())
.sources(sources)
.asset_files(discovery::list_asset_folder(input)?))
}
}
pub fn try_project_options(input: &Path) -> Result<CompileOptions, ProjectOptionsError> {
let asset_files = discovery::list_asset_folder(input)?;
Ok(match project_sources(input)? {
(Some(paths), sources) => {
let workers = project::try_read_workers_config_with(input, &manifest_overlay(input))?;
CompileOptions::split(input.to_path_buf(), paths)
.sources(sources)
.compatibility_flags(workers.compatibility_flags)
.asset_files(asset_files)
}
(None, sources) => CompileOptions::single(input.to_path_buf())
.sources(sources)
.asset_files(asset_files),
})
}
pub fn project_source_files(input: &Path) -> Result<Vec<PathBuf>, ProjectOptionsError> {
let (_, sources) = project_sources(input)?;
let mut files: Vec<PathBuf> = sources.into_keys().collect();
files.sort();
Ok(files)
}
fn project_sources(
input: &Path,
) -> Result<(Option<project::ProjectPaths>, HashMap<PathBuf, String>), ProjectOptionsError> {
if input.join("bynk.toml").exists() || input.join("src").is_dir() {
let overlay = manifest_overlay(input);
project::check_manifest(input, &overlay)?;
if project::is_workspace_root(input) {
return Err(ProjectPathsError::WorkspaceRoot.into());
}
let paths = try_read_project_paths_with(input, &overlay)?;
let sources = split_sources(input, &paths)?;
Ok((Some(paths), sources))
} else {
Ok((None, discovery::read_bynk_tree_single(input)?))
}
}
fn manifest_overlay(input: &Path) -> HashMap<PathBuf, String> {
let toml_path = input.join("bynk.toml");
match fs::read_to_string(&toml_path) {
Ok(text) => HashMap::from([(toml_path, text)]),
Err(_) => HashMap::new(),
}
}
fn options_for_split(
input: &Path,
paths: project::ProjectPaths,
) -> Result<CompileOptions, discovery::DiscoveryError> {
let sources = split_sources(input, &paths)?;
Ok(CompileOptions::split(input.to_path_buf(), paths).sources(sources))
}
fn split_sources(
input: &Path,
paths: &project::ProjectPaths,
) -> Result<HashMap<PathBuf, String>, discovery::DiscoveryError> {
let roots = project::Roots::Split {
project_root: input.to_path_buf(),
paths: paths.clone(),
};
discovery::sources_for_roots(&roots)
}
#[derive(Debug)]
pub enum ProjectOptionsError {
Paths(ProjectPathsError),
Discovery(discovery::DiscoveryError),
}
impl std::fmt::Display for ProjectOptionsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Paths(e) => write!(f, "{e}"),
Self::Discovery(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for ProjectOptionsError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Paths(_) => None,
Self::Discovery(e) => Some(e),
}
}
}
impl From<ProjectPathsError> for ProjectOptionsError {
fn from(e: ProjectPathsError) -> Self {
Self::Paths(e)
}
}
impl From<discovery::DiscoveryError> for ProjectOptionsError {
fn from(e: discovery::DiscoveryError) -> Self {
Self::Discovery(e)
}
}
pub fn print_project_failure(failure: &project::ProjectFailure) {
for ae in &failure.errors {
match attributed_snapshot(ae, &failure.snapshots, &failure.display_root) {
Some((label, text)) => {
print_attributed(ae, &label, text, &failure.snapshots, &failure.display_root);
}
None => {
eprintln!("[{}] {}", ae.error.category, ae.error.message);
for note in &ae.error.notes {
eprintln!(" note: {note}");
}
for (_, label) in &ae.error.labels {
eprintln!(" label: {label}");
}
}
}
}
}
pub fn print_project_warnings(
warnings: &[project::AttributedError],
snapshots: &[(PathBuf, String)],
display_root: &Path,
) {
for w in warnings {
match attributed_snapshot(w, snapshots, display_root) {
Some((label, text)) => print_attributed(w, &label, text, snapshots, display_root),
None => {
let where_ = w
.source_path
.as_deref()
.map(|p| format!("{}: ", p.to_string_lossy().replace('\\', "/")))
.unwrap_or_default();
eprintln!("{where_}warning[{}]: {}", w.error.category, w.error.message);
for note in &w.error.notes {
eprintln!(" note: {note}");
}
for (_, label) in &w.error.labels {
eprintln!(" label: {label}");
}
}
}
}
}
pub fn print_project_warnings_short(
warnings: &[project::AttributedError],
snapshots: &[(PathBuf, String)],
display_root: &Path,
) {
for w in warnings {
match attributed_snapshot(w, snapshots, display_root) {
Some((label, text)) => eprintln!("{}", bynk_render::short_line(&label, text, &w.error)),
None => eprintln!(
"{}[{}]: {}",
bynk_render::severity_word(&w.error),
w.error.category,
w.error.message
),
}
}
}
fn attributed_snapshot<'a>(
ae: &project::AttributedError,
snapshots: &'a [(PathBuf, String)],
display_root: &Path,
) -> Option<(String, &'a str)> {
let path = ae.source_path.as_deref()?;
let text = snapshots
.iter()
.find(|(p, _)| p.as_path() == path)
.map(|(_, t)| t.as_str())?;
Some((display_path(display_root, path), text))
}
fn print_attributed(
ae: &project::AttributedError,
label: &str,
text: &str,
snapshots: &[(PathBuf, String)],
display_root: &Path,
) {
let others: Vec<Option<(String, &str)>> = (0..ae.error.labels.len())
.map(|i| match ae.label_file(i) {
project::LabelFile::Other(path) => snapshots
.iter()
.find(|(p, _)| p == path)
.map(|(_, t)| (display_path(display_root, path), t.as_str())),
project::LabelFile::FirstParty {
identity_path,
source,
} => Some(((*identity_path).to_string(), *source)),
_ => None,
})
.collect();
let homes: Vec<LabelHome<'_>> = others
.iter()
.enumerate()
.map(|(i, other)| match (ae.label_file(i), other) {
(project::LabelFile::Primary, _) => LabelHome::Primary,
(_, Some((filename, source))) => LabelHome::File { filename, source },
(_, None) => LabelHome::Unavailable,
})
.collect();
bynk_render::print_error_in(&ae.error, text, label, &homes);
}
pub fn display_path(display_root: &Path, identity: &Path) -> String {
use std::path::Component;
let joined: PathBuf = display_root
.join(identity)
.components()
.filter(|c| !matches!(c, Component::CurDir))
.collect();
let shown = if joined.is_absolute() {
let cwd = std::env::current_dir().ok();
let canonical_cwd = cwd.as_ref().and_then(|c| c.canonicalize().ok());
cwd.iter()
.chain(canonical_cwd.iter())
.find_map(|base| joined.strip_prefix(base).ok().map(Path::to_path_buf))
.unwrap_or(joined)
} else {
joined
};
let text = shown.to_string_lossy().replace('\\', "/");
if let Some(unc) = text.strip_prefix("//?/UNC/") {
format!("//{unc}")
} else if let Some(rest) = text.strip_prefix("//?/") {
rest.to_string()
} else {
text
}
}
fn shown(path: &Path) -> String {
display_path(Path::new(""), path)
}
pub fn print_project_failure_short(failure: &project::ProjectFailure) {
for line in project_failure_short_lines(failure) {
eprintln!("{line}");
}
}
pub fn project_failure_short_lines(failure: &project::ProjectFailure) -> Vec<String> {
failure
.errors
.iter()
.map(
|ae| match attributed_snapshot(ae, &failure.snapshots, &failure.display_root) {
Some((label, text)) => bynk_render::short_line(&label, text, &ae.error),
None => format!(
"{}[{}]: {}",
bynk_render::severity_word(&ae.error),
ae.error.category,
ae.error.message
),
},
)
.collect()
}
pub fn print_project_check(check: &project::ProjectCheck) {
for ae in &check.errors {
match attributed_snapshot(ae, &check.snapshots, &check.display_root) {
Some((label, text)) => {
print_attributed(ae, &label, text, &check.snapshots, &check.display_root);
}
None => {
eprintln!(
"{}[{}]: {}",
bynk_render::severity_word(&ae.error),
ae.error.category,
ae.error.message
);
for note in &ae.error.notes {
eprintln!(" note: {note}");
}
for (_, label) in &ae.error.labels {
eprintln!(" label: {label}");
}
}
}
}
}
pub fn project_check_short_lines(check: &project::ProjectCheck) -> Vec<String> {
check
.errors
.iter()
.map(
|ae| match attributed_snapshot(ae, &check.snapshots, &check.display_root) {
Some((label, text)) => bynk_render::short_line(&label, text, &ae.error),
None => format!(
"{}[{}]: {}",
bynk_render::severity_word(&ae.error),
ae.error.category,
ae.error.message
),
},
)
.collect()
}
pub fn print_project_check_short(check: &project::ProjectCheck) {
for line in project_check_short_lines(check) {
eprintln!("{line}");
}
}
#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
pub enum IndentKind {
Tab,
Spaces,
}
#[derive(clap::Args, Debug)]
pub struct FmtArgs {
pub inputs: Vec<PathBuf>,
#[arg(long)]
pub check: bool,
#[arg(long, value_enum)]
pub indent: Option<IndentKind>,
#[arg(long, value_name = "N", value_parser = clap::value_parser!(u8).range(0..=64))]
pub indent_width: Option<u8>,
#[arg(long, value_name = "COLUMNS", value_parser = clap::value_parser!(u32).range(1..))]
pub max_line_width: Option<u32>,
#[arg(long, overrides_with = "no_trailing_comma")]
pub trailing_comma: bool,
#[arg(long, overrides_with = "trailing_comma")]
pub no_trailing_comma: bool,
#[arg(long)]
pub no_config: bool,
}
impl FmtArgs {
pub fn apply_to(&self, base: FormatOptions) -> Result<FormatOptions, String> {
let base_width = match base.indent {
IndentStyle::Spaces(n) => Some(n),
IndentStyle::Tab => None,
};
let kind = self.indent.unwrap_or(match base.indent {
IndentStyle::Tab => IndentKind::Tab,
IndentStyle::Spaces(_) => IndentKind::Spaces,
});
let indent = match (kind, self.indent_width) {
(IndentKind::Tab, None) => IndentStyle::Tab,
(IndentKind::Tab, Some(_)) => {
return Err(
"`--indent-width` applies only to spaces indentation, and this run resolves \
to tabs (pass `--indent spaces`, or set `[fmt] indent` in bynk.toml)"
.to_string(),
);
}
(IndentKind::Spaces, width) => IndentStyle::Spaces(width.or(base_width).unwrap_or(2)),
};
Ok(FormatOptions {
indent,
max_line_width: self.max_line_width.unwrap_or(base.max_line_width),
trailing_comma: if self.no_trailing_comma {
false
} else if self.trailing_comma {
true
} else {
base.trailing_comma
},
})
}
}
enum FmtOptionsError {
Manifest(PathBuf, bynk_fmt::ConfigError),
ManifestTables(PathBuf, project::ProjectPathsError),
Args(String),
}
impl std::fmt::Display for FmtOptionsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Manifest(path, e) => write!(f, "{}: {e}", path.display()),
Self::ManifestTables(path, e) => write!(f, "{}: {e}", path.display()),
Self::Args(e) => write!(f, "{e}"),
}
}
}
struct ManifestCache {
disabled: bool,
by_dir: std::collections::HashMap<PathBuf, FormatOptions>,
}
impl ManifestCache {
fn new(disabled: bool) -> Self {
Self {
disabled,
by_dir: std::collections::HashMap::new(),
}
}
fn options_for(&mut self, input: &Path) -> Result<FormatOptions, FmtOptionsError> {
if self.disabled {
return Ok(FormatOptions::default());
}
let start: PathBuf = if input.as_os_str() == "-" {
PathBuf::from(".")
} else {
match input.parent() {
Some(p) if !p.as_os_str().is_empty() => p.to_path_buf(),
_ => PathBuf::from("."),
}
};
let start = std::env::current_dir()
.map(|cwd| cwd.join(&start))
.unwrap_or(start);
if let Some(hit) = self.by_dir.get(&start) {
return Ok(*hit);
}
let opts = match bynk_fmt::find_manifest(&start) {
None => FormatOptions::default(),
Some(manifest) => {
let text = std::fs::read_to_string(&manifest).map_err(|e| {
FmtOptionsError::Manifest(
manifest.clone(),
bynk_fmt::ConfigError::Read(e.to_string()),
)
})?;
let config = bynk_fmt::FmtConfig::from_manifest_str(&text)
.map_err(|e| FmtOptionsError::Manifest(manifest.clone(), e))?;
let options = config.apply(FormatOptions::default());
let root = manifest.parent().unwrap_or(Path::new("."));
project::check_manifest(root, &HashMap::from([(root.join("bynk.toml"), text)]))
.map_err(|e| FmtOptionsError::ManifestTables(manifest, e))?;
options
}
};
self.by_dir.insert(start, opts);
Ok(opts)
}
}
pub fn run_fmt(prog: &str, args: &FmtArgs) -> ExitCode {
let check = args.check;
if args.inputs.is_empty() {
eprintln!("{prog} fmt: no input files (pass file or directory paths, or `-` for stdin)");
return ExitCode::FAILURE;
}
let inputs = match expand_fmt_inputs(&args.inputs) {
Ok(inputs) => inputs,
Err(e) => {
eprintln!("{prog} fmt: {e}");
return ExitCode::FAILURE;
}
};
let inputs = &inputs;
let mut manifests = ManifestCache::new(args.no_config);
let mut resolved: Vec<FormatOptions> = Vec::with_capacity(inputs.len());
for input in inputs {
match manifests
.options_for(input)
.and_then(|base| args.apply_to(base).map_err(FmtOptionsError::Args))
{
Ok(opts) => resolved.push(opts),
Err(e) => {
eprintln!("{prog} fmt: {e}");
return ExitCode::FAILURE;
}
}
}
let mut had_diff = false;
let mut had_error = false;
for (input, opts) in inputs.iter().zip(resolved) {
if input.as_os_str() == "-" {
use std::io::Read;
let mut source = String::new();
if let Err(e) = std::io::stdin().read_to_string(&mut source) {
eprintln!("{prog} fmt: read from stdin: {e}");
return ExitCode::FAILURE;
}
let source = bynk_fmt::normalize_line_endings(&source);
match format_source(&source, &opts) {
Ok(formatted) => {
if check {
if formatted != source {
eprintln!("{prog} fmt: <stdin> is not canonically formatted");
had_diff = true;
}
} else {
print!("{formatted}");
}
}
Err(e) => {
bynk_render::print_errors(&e.errors, &source, "<stdin>");
return ExitCode::FAILURE;
}
}
continue;
}
let raw = match std::fs::read_to_string(input) {
Ok(s) => s,
Err(e) => {
eprintln!("{prog} fmt: read `{}`: {e}", shown(input));
had_error = true;
continue;
}
};
let source = bynk_fmt::normalize_line_endings(&raw);
let filename = shown(input);
match format_source(&source, &opts) {
Ok(formatted) => {
if check {
if formatted != source {
eprintln!("{prog} fmt: {} is not canonically formatted", filename);
had_diff = true;
}
} else if formatted != source
&& let Err(e) = atomic_write(input, &formatted)
{
eprintln!("{prog} fmt: write `{}`: {e}", filename);
had_error = true;
}
}
Err(e) => {
bynk_render::print_errors(&e.errors, &source, &filename);
had_error = true;
}
}
}
if had_error || (check && had_diff) {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
}
fn expand_fmt_inputs(inputs: &[PathBuf]) -> Result<Vec<PathBuf>, String> {
let mut out = Vec::new();
let mut seen = std::collections::HashSet::new();
for input in inputs {
if input.as_os_str() != "-"
&& let Some(workspace) = workspace_at(input)
{
let workspace = workspace.map_err(|e| format!("`{}`: {e}", input.display()))?;
for member in &workspace.members {
let member = member_dir(input, member);
let files = project_source_files(&member)
.map_err(|e| format!("`{}`: {e}", shown(&member)))?;
for file in files {
let key = std::fs::canonicalize(&file).unwrap_or_else(|_| file.clone());
if seen.insert(key) {
out.push(file);
}
}
}
continue;
}
let files = if input.as_os_str() != "-" && input.is_dir() {
let files =
project_source_files(input).map_err(|e| format!("`{}`: {e}", input.display()))?;
if files.is_empty() {
return Err(format!("no `.bynk` files under `{}`", input.display()));
}
files
} else {
vec![input.clone()]
};
for file in files {
let key = std::fs::canonicalize(&file).unwrap_or_else(|_| file.clone());
if seen.insert(key) {
out.push(file);
}
}
}
Ok(out)
}
fn atomic_write(path: &Path, contents: &str) -> std::io::Result<()> {
use std::io::Write as _;
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let dir = path.parent().filter(|p| !p.as_os_str().is_empty());
let file_name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
let (mut file, tmp) = loop {
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let tmp_name = format!(".{file_name}.bynk-fmt.{}.{n}.tmp", std::process::id());
let tmp = match dir {
Some(d) => d.join(tmp_name),
None => PathBuf::from(tmp_name),
};
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&tmp)
{
Ok(f) => break (f, tmp),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(e) => return Err(e),
}
};
let write_result = (|| {
if let Ok(meta) = std::fs::metadata(path) {
let _ = file.set_permissions(meta.permissions());
}
file.write_all(contents.as_bytes())?;
file.sync_all()
})();
if let Err(e) = write_result {
let _ = std::fs::remove_file(&tmp);
return Err(e);
}
if let Err(e) = std::fs::rename(&tmp, path) {
let _ = std::fs::remove_file(&tmp);
return Err(e);
}
Ok(())
}
pub fn run_check(prog: &str, input: &Path, short: bool) -> ExitCode {
if let Some(workspace) = workspace_at(input) {
let workspace = match workspace {
Ok(w) => w,
Err(e) => {
eprintln!("{prog}: {}: {e}", shown(&input.join("bynk.toml")));
return ExitCode::FAILURE;
}
};
let mut failed = false;
for member in &workspace.members {
let member = member_dir(input, member);
if !short {
eprintln!("{}", member_header(&member));
}
failed |= !check_one(prog, &member, short, Some(&member));
}
return if failed {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
};
}
if check_one(prog, input, short, None) {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
}
}
pub fn workspace_at(input: &Path) -> Option<Result<project::Workspace, ProjectPathsError>> {
(input.is_dir() && project::is_workspace_root(input)).then(|| project::load_workspace(input))
}
pub fn member_dir(root: &Path, member: &Path) -> PathBuf {
root.join(member)
.components()
.filter(|c| !matches!(c, std::path::Component::CurDir))
.collect()
}
pub fn member_header(member: &Path) -> String {
format!("── {} ──", shown(member))
}
fn check_one(prog: &str, input: &Path, short: bool, member: Option<&Path>) -> bool {
let prog = match member {
Some(m) => format!("{prog}: {}", shown(m)),
None => prog.to_string(),
};
let prog = prog.as_str();
if input.is_dir() {
let options = match try_project_options(input) {
Ok(o) => o,
Err(e) => {
eprintln!("{prog}: {e}");
return false;
}
};
let options = match schema_lock::read_only(input) {
Ok(lock) => options.schema_registry(lock),
Err(e) => {
eprintln!("{prog}: {e}");
return false;
}
};
let check = project::check_project(&options);
let has_errors = check.has_errors();
if short {
print_project_check_short(&check);
} else {
print_project_check(&check);
}
!has_errors
} else {
let source = match std::fs::read_to_string(input) {
Ok(s) => s,
Err(e) => {
eprintln!("{prog}: could not read `{}`: {e}", shown(input));
return false;
}
};
let filename = shown(input);
match bynk_emit::check_with_warnings(&source, &filename) {
Ok(warnings) => {
if !warnings.is_empty() {
if short {
bynk_render::print_errors_short(&warnings, &source, &filename);
} else {
bynk_render::print_errors(&warnings, &source, &filename);
}
}
true
}
Err(errors) => {
if short {
bynk_render::print_errors_short(&errors, &source, &filename);
} else {
bynk_render::print_errors(&errors, &source, &filename);
}
false
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[derive(clap::Parser, Debug)]
struct Harness {
#[command(flatten)]
args: FmtArgs,
}
fn parse(argv: &[&str]) -> FmtArgs {
let mut full = vec!["fmt"];
full.extend_from_slice(argv);
Harness::parse_from(full).args
}
fn resolve(argv: &[&str]) -> Result<FormatOptions, String> {
parse(argv).apply_to(FormatOptions::default())
}
fn manifest(toml: &str) -> FormatOptions {
bynk_fmt::FmtConfig::from_manifest_str(toml)
.expect("manifest parses")
.apply(FormatOptions::default())
}
#[test]
fn no_flags_is_the_canonical_style() {
let opts = resolve(&["a.bynk"]).expect("valid");
assert_eq!(opts, FormatOptions::default());
}
#[test]
fn spaces_without_a_width_falls_back_to_two() {
let opts = resolve(&["--indent", "spaces", "a.bynk"]).expect("valid");
assert_eq!(opts.indent, IndentStyle::Spaces(2));
}
#[test]
fn spaces_takes_the_given_width() {
let opts =
resolve(&["--indent", "spaces", "--indent-width", "4", "a.bynk"]).expect("valid");
assert_eq!(opts.indent, IndentStyle::Spaces(4));
}
#[test]
fn a_width_with_tabs_is_an_error_not_a_silent_no_op() {
let err = resolve(&["--indent", "tab", "--indent-width", "4", "a.bynk"])
.expect_err("a meaningless width must be reported");
assert!(err.contains("--indent-width"), "{err}");
assert!(err.contains("spaces"), "{err}");
}
#[test]
fn the_trailing_comma_pair_is_last_one_wins() {
assert!(
resolve(&["--no-trailing-comma", "--trailing-comma", "a.bynk"])
.expect("valid")
.trailing_comma
);
assert!(
!resolve(&["--trailing-comma", "--no-trailing-comma", "a.bynk"])
.expect("valid")
.trailing_comma
);
}
#[test]
fn max_line_width_is_taken_verbatim_and_zero_is_refused() {
assert_eq!(
resolve(&["--max-line-width", "60", "a.bynk"])
.expect("valid")
.max_line_width,
60
);
assert!(
Harness::try_parse_from(["fmt", "--max-line-width", "0", "a.bynk"]).is_err(),
"`--max-line-width 0` must not parse"
);
}
#[test]
fn an_unflagged_run_takes_the_manifest_whole() {
let base = manifest(
"[fmt]\nindent = \"spaces\"\nindent_width = 4\nmax_line_width = 120\ntrailing_comma = false\n",
);
let opts = parse(&["a.bynk"]).apply_to(base).expect("valid");
assert_eq!(opts.indent, IndentStyle::Spaces(4));
assert_eq!(opts.max_line_width, 120);
assert!(!opts.trailing_comma);
}
#[test]
fn a_flag_beats_the_manifest_field_it_names_and_no_other() {
let base = manifest("[fmt]\nindent = \"spaces\"\nindent_width = 4\nmax_line_width = 120\n");
let opts = parse(&["--max-line-width", "80", "a.bynk"])
.apply_to(base)
.expect("valid");
assert_eq!(opts.max_line_width, 80, "the flag wins where it speaks");
assert_eq!(
opts.indent,
IndentStyle::Spaces(4),
"and stays silent everywhere else"
);
}
#[test]
fn an_absent_flag_does_not_reset_the_manifest_to_the_default() {
let base = manifest("[fmt]\nmax_line_width = 120\n");
assert_eq!(
parse(&["a.bynk"])
.apply_to(base)
.expect("valid")
.max_line_width,
120
);
}
#[test]
fn indent_spaces_alone_keeps_the_manifest_width() {
let base = manifest("[fmt]\nindent = \"spaces\"\nindent_width = 4\n");
let opts = parse(&["--indent", "spaces", "a.bynk"])
.apply_to(base)
.expect("valid");
assert_eq!(opts.indent, IndentStyle::Spaces(4), "not reset to 2");
}
#[test]
fn indent_width_alone_applies_to_a_manifest_that_chose_spaces() {
let base = manifest("[fmt]\nindent = \"spaces\"\n");
let opts = parse(&["--indent-width", "8", "a.bynk"])
.apply_to(base)
.expect("valid");
assert_eq!(opts.indent, IndentStyle::Spaces(8));
}
#[test]
fn indent_width_alone_is_refused_when_the_run_resolves_to_tabs() {
let err = parse(&["--indent-width", "8", "a.bynk"])
.apply_to(manifest("[fmt]\nindent = \"tab\"\n"))
.expect_err("refused");
assert!(err.contains("resolves"), "{err}");
}
#[test]
fn an_explicit_tab_flag_overrides_a_manifest_choosing_spaces() {
let base = manifest("[fmt]\nindent = \"spaces\"\nindent_width = 4\n");
let opts = parse(&["--indent", "tab", "a.bynk"])
.apply_to(base)
.expect("valid");
assert_eq!(opts.indent, IndentStyle::Tab);
}
#[test]
fn trailing_comma_flag_overrides_a_manifest_that_turned_it_off() {
let base = manifest("[fmt]\ntrailing_comma = false\n");
assert!(
parse(&["--trailing-comma", "a.bynk"])
.apply_to(base)
.expect("valid")
.trailing_comma
);
assert!(
!parse(&["a.bynk"])
.apply_to(base)
.expect("valid")
.trailing_comma
);
}
#[test]
fn no_config_is_parsed_and_defaults_are_used_in_its_presence() {
let args = parse(&["--no-config", "a.bynk"]);
assert!(args.no_config);
assert_eq!(
args.apply_to(FormatOptions::default()).expect("valid"),
FormatOptions::default()
);
}
struct Scratch(PathBuf);
impl Drop for Scratch {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
fn scratch_dir(tag: &str) -> Scratch {
let dir = std::env::temp_dir().join(format!(
"bynk_1077_{tag}_{}_{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
Scratch(dir)
}
#[test]
fn manifest_overlay_keys_and_reads_a_real_bynk_toml() {
let dir = scratch_dir("manifest_overlay");
let toml = "[paths]\ninclude = [\"lib\"]\n";
fs::write(dir.0.join("bynk.toml"), toml).unwrap();
let overlay = manifest_overlay(&dir.0);
assert_eq!(
overlay.get(&dir.0.join("bynk.toml")).map(String::as_str),
Some(toml)
);
}
#[test]
fn manifest_overlay_is_empty_with_no_bynk_toml() {
let dir = scratch_dir("manifest_overlay_missing");
assert!(manifest_overlay(&dir.0).is_empty());
}
#[test]
fn try_project_options_surfaces_an_unknown_paths_key() {
let dir = scratch_dir("try_project_options_unknown_key");
fs::write(dir.0.join("bynk.toml"), "[paths]\ninculde = [\"src\"]\n").unwrap();
fs::create_dir_all(dir.0.join("src")).unwrap();
fs::write(dir.0.join("src/thing.bynk"), "context thing\n").unwrap();
let err = match try_project_options(&dir.0) {
Err(e) => e,
Ok(_) => panic!("an unrecognised [paths] key must be reported, not silently ignored"),
};
assert!(
matches!(
&err,
ProjectOptionsError::Paths(ProjectPathsError::UnknownKey(k)) if k == "inculde"
),
"expected Paths(UnknownKey(\"inculde\")), got: {err:?}"
);
}
#[test]
fn try_project_options_surfaces_an_unknown_table() {
let dir = scratch_dir("try_project_options_unknown_table");
fs::write(
dir.0.join("bynk.toml"),
"[paths]\ninclude = [\"src\"]\n\n[pahts]\nexclude = []\n",
)
.unwrap();
fs::create_dir_all(dir.0.join("src")).unwrap();
fs::write(dir.0.join("src/thing.bynk"), "context thing\n").unwrap();
let err = match try_project_options(&dir.0) {
Err(e) => e,
Ok(_) => panic!("an unknown table must be reported, not silently ignored"),
};
assert!(
matches!(
&err,
ProjectOptionsError::Paths(ProjectPathsError::UnknownTable(t)) if t == "pahts"
),
"expected Paths(UnknownTable(\"pahts\")), got: {err:?}"
);
assert!(err.to_string().contains("did you mean `[paths]`?"), "{err}");
}
fn workers_project(name: &str, manifest: &str) -> Scratch {
let dir = scratch_dir(name);
fs::write(dir.0.join("bynk.toml"), manifest).unwrap();
fs::create_dir_all(dir.0.join("src")).unwrap();
fs::write(
dir.0.join("src/greet.bynk"),
"context greet\n\nservice hello {\n on call(name: String) -> Effect[Result[String, ()]] {\n Ok(name)\n }\n}\n",
)
.unwrap();
dir
}
fn greet_wrangler(options: CompileOptions) -> (String, Vec<&'static str>) {
let out = project::compile_project(&options.target(project::BuildTarget::Workers))
.unwrap_or_else(|f| {
let msgs: Vec<&str> = f.errors.iter().map(|e| e.error.message.as_str()).collect();
panic!("builds: {msgs:?}")
});
let text = out.artefacts.docs[Path::new("workers/greet/wrangler.toml")].text();
(
text,
out.warnings.iter().map(|w| w.error.category).collect(),
)
}
#[test]
fn manifest_compatibility_flags_reach_wrangler_toml() {
let dir = workers_project(
"compat_flags_1890",
"[workers]\ncompatibility_flags = [\"nodejs_compat\", \"global_fetch_strictly_public\"]\n",
);
let want = "compatibility_flags = [\"global_fetch_strictly_public\", \"nodejs_compat\"]\n";
for options in [
try_project_options(&dir.0).expect("reads"),
project_options(&dir.0).expect("reads"),
] {
let (text, warnings) = greet_wrangler(options);
assert!(text.contains(want), "{text}");
assert_eq!(
warnings,
["bynk.project.duplicate_compatibility_flag"],
"the repeated default is reported"
);
}
}
#[test]
fn a_worker_gets_the_default_compatibility_flag_without_a_workers_table() {
let dir = workers_project("compat_flags_default_1890", "[project]\nname = \"p\"\n");
let (text, warnings) = greet_wrangler(try_project_options(&dir.0).expect("reads"));
assert!(
text.contains("compatibility_flags = [\"global_fetch_strictly_public\"]\n"),
"{text}"
);
assert!(warnings.is_empty(), "{warnings:?}");
}
#[test]
fn try_project_options_refuses_a_non_string_compatibility_flag() {
let dir = workers_project(
"compat_flags_bad_1890",
"[workers]\ncompatibility_flags = [\"nodejs_compat\", 2]\n",
);
match try_project_options(&dir.0) {
Err(ProjectOptionsError::Paths(ProjectPathsError::NotAStringList { .. })) => {}
Err(e) => panic!("wrong error: {e:?}"),
Ok(_) => panic!("a non-string flag must be refused"),
}
}
}
#[cfg(test)]
mod display_path_tests {
use super::display_path;
use std::path::Path;
#[test]
fn a_relative_root_is_joined_as_typed() {
assert_eq!(
display_path(Path::new("test/fixtures/x"), Path::new("src/a.bynk")),
"test/fixtures/x/src/a.bynk"
);
}
#[test]
fn a_dot_root_adds_no_prefix() {
assert_eq!(
display_path(Path::new("."), Path::new("src/a.bynk")),
"src/a.bynk"
);
assert_eq!(
display_path(Path::new(""), Path::new("./src/a.bynk")),
"src/a.bynk"
);
}
#[test]
fn an_absolute_root_inside_the_cwd_is_shown_relative() {
let cwd = std::env::current_dir().unwrap();
assert_eq!(
display_path(&cwd.join("proj"), Path::new("src/a.bynk")),
"proj/src/a.bynk"
);
let canonical = cwd.canonicalize().unwrap();
assert_eq!(
display_path(&canonical.join("proj"), Path::new("src/a.bynk")),
"proj/src/a.bynk"
);
}
#[test]
fn an_absolute_root_outside_the_cwd_is_shown_absolute() {
let cwd = std::env::current_dir().unwrap();
let outside = cwd.parent().unwrap().join("bynk-display-path-elsewhere");
let shown = display_path(&outside, Path::new("src/a.bynk"));
assert!(
shown.ends_with("bynk-display-path-elsewhere/src/a.bynk"),
"{shown}"
);
assert!(
!shown.contains('\\') && !shown.starts_with("//?/"),
"{shown}"
);
}
#[cfg(windows)]
#[test]
fn a_verbatim_root_outside_the_cwd_loses_its_prefix() {
assert_eq!(
display_path(Path::new(r"\\?\C:\elsewhere"), Path::new("src/a.bynk")),
"C:/elsewhere/src/a.bynk"
);
}
}