//! `code_comments` — comment density per language across tracked source files. Pairs with
//! `code_loc` (which counts physical lines) to surface the documentation posture of the
//! codebase: "Rust 18% comments / TypeScript 4%" tells you which language family carries
//! the team's writing.
use std::collections::HashMap;
use async_trait::async_trait;
use sha2::{Digest, Sha256};
use crate::options::OptionSchema;
use crate::payload::{
BadgeData, Bar, BarsData, Body, EntriesData, Entry, MarkdownTextBlockData, Payload, RatioData,
Status, TextBlockData, TextData,
};
use crate::render::Shape;
use crate::samples;
use super::super::git::{open_repo, payload, repo_cache_key};
use super::super::{FetchContext, FetchError, Fetcher, Safety};
use super::scan::for_each_tokei_stat;
const SHAPES: &[Shape] = &[
Shape::Text,
Shape::TextBlock,
Shape::MarkdownTextBlock,
Shape::Entries,
Shape::Bars,
Shape::Ratio,
Shape::Badge,
];
const DEFAULT_LIMIT: usize = 10;
const OPTION_SCHEMAS: &[OptionSchema] = &[
OptionSchema {
name: "limit",
type_hint: "integer",
required: false,
default: Some("10"),
description: "Cap on rendered languages (`TextBlock` / `MarkdownTextBlock` / `Entries` / `Bars`). The `Text` summary always reports the whole-repo ratio.",
},
OptionSchema {
name: "unit",
type_hint: "`percent` (alias `%`) | `loc` | `kloc`",
required: false,
default: Some("percent"),
description: "Display format for per-language values: `percent` (`18.3%` of that language's `code+comments`), `loc` (raw comment lines, `1,234`), or `kloc` (`1.2k`). `Ratio` always emits the whole-repo comment share regardless of this option; `Badge` always reports tier + percent.",
},
];
pub struct CodeComments;
#[derive(Debug, Default, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct Options {
#[serde(default)]
limit: Option<usize>,
#[serde(default)]
unit: Option<Unit>,
}
#[derive(Debug, Clone, Copy, Default, serde::Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
enum Unit {
#[default]
#[serde(alias = "%")]
Percent,
Loc,
Kloc,
}
#[async_trait]
impl Fetcher for CodeComments {
fn name(&self) -> &str {
"code_comments"
}
fn safety(&self) -> Safety {
Safety::Safe
}
fn description(&self) -> &'static str {
"Comment-density per language across tracked source files in the discovered git repo. tokei parses each file into `code` / `comments` / `blanks`; this fetcher surfaces the comment share. `Text` headlines the whole-repo ratio; `TextBlock` / `MarkdownTextBlock` / `Entries` / `Bars` rank per-language values (default `percent`, override with `unit = loc | kloc`); `Ratio` exposes the whole-repo share for gauges; `Badge` tiers documentation posture (`undocumented` / `light` / `balanced` / `documented` / `verbose`)."
}
fn refresh_interval(&self) -> u64 {
60 * 10
}
fn shapes(&self) -> &[Shape] {
SHAPES
}
fn default_shape(&self) -> Shape {
Shape::Text
}
fn cache_key(&self, ctx: &FetchContext) -> String {
let base = repo_cache_key(self.name(), ctx);
let opts = ctx
.options
.as_ref()
.map(toml::Value::to_string)
.unwrap_or_default();
if opts.is_empty() {
return base;
}
let digest = Sha256::digest(opts.as_bytes());
let hex: String = digest.iter().take(4).map(|b| format!("{b:02x}")).collect();
format!("{base}-{hex}")
}
fn option_schemas(&self) -> &[OptionSchema] {
OPTION_SCHEMAS
}
fn sample_body(&self, shape: Shape) -> Option<Body> {
Some(match shape {
Shape::Text => samples::text("18.3% comments · 3,456 / 18,892 lines"),
Shape::TextBlock => samples::text_block(&[
"Rust 24.1%",
"Markdown 58.0%",
"TypeScript 9.4%",
"TOML 4.2%",
]),
Shape::MarkdownTextBlock => samples::markdown(
"- **Rust** 24.1%\n- **Markdown** 58.0%\n- **TypeScript** 9.4%\n- **TOML** 4.2%",
),
Shape::Entries => samples::entries(&[
("Rust", "24.1%"),
("Markdown", "58.0%"),
("TypeScript", "9.4%"),
("TOML", "4.2%"),
]),
Shape::Bars => samples::bars(&[
("Rust", 1900),
("Markdown", 580),
("TypeScript", 230),
("TOML", 32),
]),
Shape::Ratio => samples::ratio(0.183, "comments"),
Shape::Badge => samples::badge(Status::Ok, "balanced · 18%"),
_ => return None,
})
}
async fn fetch(&self, ctx: &FetchContext) -> Result<Payload, FetchError> {
let opts: Options = parse_options(ctx.options.as_ref())?;
let limit = opts.limit.filter(|n| *n > 0).unwrap_or(DEFAULT_LIMIT);
let unit = opts.unit.unwrap_or_default();
let totals = scan_cwd()?;
Ok(payload(render_body(
totals,
ctx.shape.unwrap_or(Shape::Text),
limit,
unit,
)))
}
}
fn parse_options(raw: Option<&toml::Value>) -> Result<Options, FetchError> {
match raw {
None => Ok(Options::default()),
Some(value) => value
.clone()
.try_into::<Options>()
.map_err(|e| FetchError::Failed(format!("invalid options: {e}"))),
}
}
#[derive(Debug, Default, Clone)]
struct LangStat {
code: u64,
comments: u64,
}
#[derive(Debug, Default)]
struct Totals {
/// Sorted by comment ratio desc (then by name asc), languages with zero `code+comments` dropped.
by_language: Vec<(String, LangStat)>,
total_code: u64,
total_comments: u64,
}
fn scan_cwd() -> Result<Totals, FetchError> {
let repo = open_repo()?;
scan_repo(&repo)
}
fn scan_repo(repo: &gix::Repository) -> Result<Totals, FetchError> {
let mut by: HashMap<&'static str, LangStat> = HashMap::new();
let mut total_code = 0u64;
let mut total_comments = 0u64;
for_each_tokei_stat(repo, |_path, name, stats| {
// Prose languages (Markdown / MDX / Plain Text / AsciiDoc) report every line as
// `comments` and zero `code`, which makes their comment-density ratio collapse to
// 100% and dominates the per-language ranking. They aren't documenting code, so
// they don't belong in a code-comment-density metric. Filtering on `code > 0`
// also drops empty / header-only files that would distort small-language ratios.
if stats.code == 0 {
return;
}
let entry = by.entry(name).or_default();
entry.code += stats.code as u64;
entry.comments += stats.comments as u64;
total_code += stats.code as u64;
total_comments += stats.comments as u64;
})?;
let mut by_language: Vec<(String, LangStat)> = by
.into_iter()
.filter(|(_, s)| s.code + s.comments > 0)
.map(|(name, stat)| (name.to_string(), stat))
.collect();
by_language.sort_by(|a, b| {
ratio_of(&b.1)
.partial_cmp(&ratio_of(&a.1))
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.0.cmp(&b.0))
});
Ok(Totals {
by_language,
total_code,
total_comments,
})
}
fn ratio_of(stat: &LangStat) -> f64 {
let denom = stat.code + stat.comments;
if denom == 0 {
0.0
} else {
stat.comments as f64 / denom as f64
}
}
fn render_body(totals: Totals, shape: Shape, limit: usize, unit: Unit) -> Body {
match shape {
Shape::TextBlock => Body::TextBlock(TextBlockData {
lines: totals
.by_language
.iter()
.take(limit)
.map(|(name, stat)| format!("{:<12} {}", name, format_value(stat, unit)))
.collect(),
}),
Shape::MarkdownTextBlock => Body::MarkdownTextBlock(MarkdownTextBlockData {
value: totals
.by_language
.iter()
.take(limit)
.map(|(name, stat)| format!("- **{name}** {}", format_value(stat, unit)))
.collect::<Vec<_>>()
.join("\n"),
}),
Shape::Entries => Body::Entries(EntriesData {
items: totals
.by_language
.iter()
.take(limit)
.map(|(name, stat)| Entry {
key: name.clone(),
value: Some(format_value(stat, unit)),
status: None,
})
.collect(),
}),
Shape::Bars => Body::Bars(BarsData {
bars: totals
.by_language
.iter()
.take(limit)
.map(|(name, stat)| Bar {
label: name.clone(),
value: bar_value(stat, unit),
value_label: None,
})
.collect(),
}),
Shape::Ratio => render_ratio(&totals),
Shape::Badge => render_badge(&totals),
_ => render_text(&totals),
}
}
fn render_text(totals: &Totals) -> Body {
let denom = totals.total_code + totals.total_comments;
if denom == 0 {
return Body::Text(TextData {
value: String::new(),
});
}
let pct = (totals.total_comments as f64 / denom as f64) * 100.0;
Body::Text(TextData {
value: format!(
"{:.1}% comments · {} / {} lines",
pct,
format_with_commas(totals.total_comments),
format_with_commas(denom),
),
})
}
fn render_ratio(totals: &Totals) -> Body {
let denom = totals.total_code + totals.total_comments;
let (value, denominator, label) = if denom == 0 {
(0.0, None, None)
} else {
(
(totals.total_comments as f64 / denom as f64).clamp(0.0, 1.0),
Some(denom),
Some("comments".into()),
)
};
Body::Ratio(RatioData {
value,
label,
denominator,
})
}
fn render_badge(totals: &Totals) -> Body {
let denom = totals.total_code + totals.total_comments;
if denom == 0 {
return Body::Badge(BadgeData {
status: Status::Ok,
label: "empty".into(),
});
}
let pct = (totals.total_comments as f64 / denom as f64) * 100.0;
let (tier, status) = tier_for(pct);
Body::Badge(BadgeData {
status,
label: format!("{tier} · {pct:.0}%"),
})
}
fn tier_for(pct: f64) -> (&'static str, Status) {
if pct < 5.0 {
("undocumented", Status::Warn)
} else if pct < 15.0 {
("light", Status::Ok)
} else if pct < 30.0 {
("balanced", Status::Ok)
} else if pct < 50.0 {
("documented", Status::Ok)
} else {
("verbose", Status::Warn)
}
}
fn format_value(stat: &LangStat, unit: Unit) -> String {
match unit {
Unit::Percent => format!("{:.1}%", ratio_of(stat) * 100.0),
Unit::Loc => format_with_commas(stat.comments),
Unit::Kloc => format_kloc(stat.comments),
}
}
fn bar_value(stat: &LangStat, unit: Unit) -> u64 {
match unit {
// Percent shape on Bars: scale to basis points (×10) so renderers using integer
// heights still differentiate "8.3%" from "8.7%".
Unit::Percent => (ratio_of(stat) * 1000.0).round() as u64,
Unit::Loc | Unit::Kloc => stat.comments,
}
}
fn format_with_commas(n: u64) -> String {
let s = n.to_string();
let bytes = s.as_bytes();
let mut out = String::with_capacity(s.len() + s.len() / 3);
for (i, b) in bytes.iter().enumerate() {
if i > 0 && (bytes.len() - i).is_multiple_of(3) {
out.push(',');
}
out.push(*b as char);
}
out
}
fn format_kloc(n: u64) -> String {
if n < 1_000 {
n.to_string()
} else if n < 1_000_000 {
format!("{:.1}k", n as f64 / 1_000.0)
} else {
format!("{:.1}M", n as f64 / 1_000_000.0)
}
}
#[cfg(test)]
mod tests {
use super::super::super::git::test_support::{commit_touching, make_repo};
use super::*;
fn ctx(shape: Option<Shape>, raw: Option<&str>) -> FetchContext {
FetchContext {
widget_id: "widget".into(),
shape,
options: raw.map(|s| toml::from_str(s).unwrap()),
..Default::default()
}
}
fn lang_stat(code: u64, comments: u64) -> LangStat {
LangStat { code, comments }
}
fn totals(items: &[(&str, u64, u64)]) -> Totals {
let total_code = items.iter().map(|(_, c, _)| c).sum();
let total_comments = items.iter().map(|(_, _, m)| m).sum();
Totals {
by_language: items
.iter()
.map(|(l, c, m)| ((*l).into(), lang_stat(*c, *m)))
.collect(),
total_code,
total_comments,
}
}
#[test]
fn empty_repo_returns_empty_totals() {
let (_tmp, repo) = make_repo();
let t = scan_repo(&repo).unwrap();
assert_eq!(t.total_code, 0);
assert_eq!(t.total_comments, 0);
assert!(t.by_language.is_empty());
}
#[test]
fn rust_file_with_line_comment_separates_code_and_comments() {
let (_tmp, repo) = make_repo();
commit_touching(&repo, "src/main.rs", "fn main() {}\n// hello\n// world\n");
let t = scan_repo(&repo).unwrap();
assert!(t.total_code >= 1);
assert!(t.total_comments >= 2);
}
#[test]
fn prose_files_with_zero_code_lines_are_excluded() {
// Markdown / Plain Text classify every line as `comments` per tokei → ratio
// collapses to 100% and pollutes the ranking. The `code > 0` filter drops them so
// the metric reflects code-comment density only.
let (_tmp, repo) = make_repo();
commit_touching(&repo, "README.md", "# title\n\nbody text\nmore prose\n");
commit_touching(&repo, "src/main.rs", "fn main() {}\n// note\n");
let t = scan_repo(&repo).unwrap();
assert!(
t.by_language.iter().all(|(n, _)| n != "Markdown"),
"Markdown should be filtered out, got {:?}",
t.by_language
);
assert!(t.by_language.iter().any(|(n, _)| n == "Rust"));
}
#[test]
fn rust_file_is_partitioned_into_code_and_comment_lines() {
let (_tmp, repo) = make_repo();
commit_touching(
&repo,
"src/main.rs",
"// header\nfn main() {}\n// trailing\n",
);
let t = scan_repo(&repo).unwrap();
let rust = t
.by_language
.iter()
.find(|(n, _)| n == "Rust")
.expect("Rust entry");
assert_eq!(rust.1.code, 1);
assert_eq!(rust.1.comments, 2);
}
/// Two languages with distinct comment ratios drive `scan_repo`'s production sort_by
/// closure (the dedicated `sort_order_picks_higher_ratio_first` test below uses an inline
/// sort and doesn't touch the closure inside `scan_repo`). A Rust file has one comment
/// against one code line (50%), while a Python file has only code (0%) — partial_cmp
/// returns Some(non-Equal) and the high-ratio entry must come first.
#[test]
fn scan_repo_sorts_languages_by_descending_comment_ratio() {
let (_tmp, repo) = make_repo();
commit_touching(&repo, "src/main.rs", "fn main() {}\n// note\n");
commit_touching(&repo, "script.py", "x = 1\ny = 2\nz = 3\n");
let t = scan_repo(&repo).unwrap();
let names: Vec<_> = t.by_language.iter().map(|(n, _)| n.as_str()).collect();
let rust = names.iter().position(|n| *n == "Rust").expect("Rust entry");
let python = names
.iter()
.position(|n| *n == "Python")
.expect("Python entry");
assert!(
rust < python,
"higher comment ratio (Rust 50%) must outrank lower (Python 0%); got {names:?}",
);
}
/// Two languages tied on comment ratio (both 0%) fall through to the secondary `then_with`
/// arm of `scan_repo`'s production sort closure, which orders by name ascending.
#[test]
fn scan_repo_sort_breaks_ratio_ties_alphabetically_by_language_name() {
let (_tmp, repo) = make_repo();
commit_touching(&repo, "src/main.rs", "fn main() {}\n");
commit_touching(&repo, "script.py", "x = 1\n");
let t = scan_repo(&repo).unwrap();
let names: Vec<_> = t.by_language.iter().map(|(n, _)| n.as_str()).collect();
let rust = names.iter().position(|n| *n == "Rust").expect("Rust entry");
let python = names
.iter()
.position(|n| *n == "Python")
.expect("Python entry");
assert!(
python < rust,
"ratio tie must order by name ascending — Python before Rust; got {names:?}",
);
}
/// Direct call to `ratio_of` with a zero-denominator stat exercises the `if denom == 0`
/// arm. Production callers can't reach it (the `code + comments > 0` filter in `scan_repo`
/// drops empty entries before they make it into `by_language`), so the only way to pin this
/// branch is to feed `ratio_of` an empty `LangStat` directly.
#[test]
fn ratio_of_returns_zero_for_empty_lang_stat() {
assert_eq!(ratio_of(&LangStat::default()), 0.0);
}
#[test]
fn sort_order_picks_higher_ratio_first() {
// Hand-built Totals — bypasses tokei's per-language quirks (Markdown / TOML
// bucketing varies across versions). Tests the sort key only.
let mut t = Totals {
by_language: vec![
("LowRatio".into(), lang_stat(900, 100)), // 10%
("HighRatio".into(), lang_stat(500, 500)), // 50%
],
total_code: 1400,
total_comments: 600,
};
t.by_language.sort_by(|a, b| {
ratio_of(&b.1)
.partial_cmp(&ratio_of(&a.1))
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.0.cmp(&b.0))
});
assert_eq!(t.by_language[0].0, "HighRatio");
assert_eq!(t.by_language[1].0, "LowRatio");
}
#[test]
fn text_reports_whole_repo_ratio_and_counts() {
let body = render_text(&totals(&[("Rust", 800, 200), ("Markdown", 200, 50)]));
assert!(matches!(
body,
Body::Text(d)
if d.value.contains("20.0%")
&& d.value.contains("250")
&& d.value.contains("1,250"),
));
}
#[test]
fn text_is_empty_when_no_lines() {
let body = render_text(&Totals::default());
assert!(matches!(body, Body::Text(d) if d.value.is_empty()));
}
#[test]
fn ratio_uses_total_comments_over_total_code_plus_comments() {
let body = render_ratio(&totals(&[("Rust", 800, 200)]));
assert!(matches!(
body,
Body::Ratio(d)
if (d.value - 0.2).abs() < 1e-9
&& d.denominator == Some(1000)
&& d.label.as_deref() == Some("comments"),
));
}
#[test]
fn ratio_handles_empty() {
let body = render_ratio(&Totals::default());
assert!(matches!(
body,
Body::Ratio(d)
if d.value == 0.0 && d.denominator.is_none() && d.label.is_none(),
));
}
/// `render_body(Shape::Ratio, ...)` delegates to `render_ratio`; covers the otherwise-skipped
/// `Shape::Ratio` arm of the dispatch table (the dedicated `render_ratio` tests above bypass it).
#[test]
fn render_body_ratio_arm_delegates_to_render_ratio() {
let body = render_body(
totals(&[("Rust", 800, 200)]),
Shape::Ratio,
10,
Unit::Percent,
);
assert!(matches!(
body,
Body::Ratio(d)
if (d.value - 0.2).abs() < 1e-9
&& d.denominator == Some(1000)
&& d.label.as_deref() == Some("comments"),
));
}
#[test]
fn text_block_lists_per_language_values() {
// `totals()` preserves input order — feed pre-sorted by ratio desc to match what
// `scan_repo` would produce.
let body = render_body(
totals(&[("Markdown", 100, 100), ("Rust", 800, 200)]),
Shape::TextBlock,
10,
Unit::Percent,
);
assert!(matches!(
body,
Body::TextBlock(d)
if d.lines[0].contains("Markdown")
&& d.lines[0].contains("50.0%")
&& d.lines[1].contains("Rust")
&& d.lines[1].contains("20.0%"),
));
}
#[test]
fn markdown_text_block_emits_bold_list() {
let body = render_body(
totals(&[("Rust", 800, 200)]),
Shape::MarkdownTextBlock,
10,
Unit::Percent,
);
assert!(matches!(
body,
Body::MarkdownTextBlock(d) if d.value == "- **Rust** 20.0%",
));
}
#[test]
fn entries_default_to_percent() {
let body = render_body(
totals(&[("Rust", 800, 200)]),
Shape::Entries,
10,
Unit::Percent,
);
assert!(matches!(
body,
Body::Entries(d) if d.items[0].value.as_deref() == Some("20.0%"),
));
}
#[test]
fn entries_with_loc_unit_show_raw_comment_count() {
let body = render_body(
totals(&[("Rust", 800, 1234)]),
Shape::Entries,
10,
Unit::Loc,
);
assert!(matches!(
body,
Body::Entries(d) if d.items[0].value.as_deref() == Some("1,234"),
));
}
#[test]
fn bars_in_percent_mode_use_basis_points() {
let body = render_body(
totals(&[("Rust", 800, 200)]),
Shape::Bars,
10,
Unit::Percent,
);
// 20.0% × 10 = 200 basis points.
assert!(matches!(body, Body::Bars(d) if d.bars[0].value == 200));
}
#[test]
fn bars_in_loc_mode_use_raw_comments() {
let body = render_body(totals(&[("Rust", 800, 1234)]), Shape::Bars, 10, Unit::Loc);
assert!(matches!(body, Body::Bars(d) if d.bars[0].value == 1234));
}
#[test]
fn badge_tiers_by_pct() {
assert_eq!(tier_for(2.0), ("undocumented", Status::Warn));
assert_eq!(tier_for(10.0), ("light", Status::Ok));
assert_eq!(tier_for(20.0), ("balanced", Status::Ok));
assert_eq!(tier_for(40.0), ("documented", Status::Ok));
assert_eq!(tier_for(60.0), ("verbose", Status::Warn));
}
#[test]
fn badge_handles_empty() {
let body = render_body(Totals::default(), Shape::Badge, 10, Unit::Percent);
assert!(matches!(
body,
Body::Badge(d) if d.status == Status::Ok && d.label == "empty",
));
}
#[test]
fn parse_options_rejects_unknown_keys() {
let bad: toml::Value = toml::from_str(r#"unknown = 1"#).unwrap();
assert!(parse_options(Some(&bad)).is_err());
}
#[test]
fn parse_options_accepts_unit_aliases() {
let pct: toml::Value = toml::from_str(r#"unit = "%""#).unwrap();
assert_eq!(parse_options(Some(&pct)).unwrap().unit, Some(Unit::Percent));
let loc: toml::Value = toml::from_str(r#"unit = "loc""#).unwrap();
assert_eq!(parse_options(Some(&loc)).unwrap().unit, Some(Unit::Loc));
}
#[test]
fn cache_key_changes_with_options() {
let mut ctx = FetchContext {
widget_id: "w".into(),
..Default::default()
};
let a = CodeComments.cache_key(&ctx);
ctx.options = Some(toml::from_str(r#"unit = "loc""#).unwrap());
let b = CodeComments.cache_key(&ctx);
assert_ne!(a, b);
}
#[test]
fn fetcher_contract_and_samples_cover_supported_shapes() {
let fetcher = CodeComments;
let schema_names: Vec<_> = fetcher
.option_schemas()
.iter()
.map(|schema| schema.name)
.collect();
assert_eq!(fetcher.name(), "code_comments");
assert_eq!(fetcher.safety(), Safety::Safe);
assert_eq!(fetcher.default_shape(), Shape::Text);
assert!(
fetcher
.description()
.contains("Comment-density per language")
);
assert_eq!(schema_names, vec!["limit", "unit"]);
assert_eq!(fetcher.shapes(), SHAPES);
assert!(
SHAPES
.iter()
.copied()
.all(|shape| fetcher.sample_body(shape).is_some())
);
assert!(fetcher.sample_body(Shape::Image).is_none());
}
#[test]
fn render_body_falls_back_to_text_for_unsupported_shapes() {
let body = render_body(
totals(&[("Rust", 800, 200), ("TOML", 90, 10)]),
Shape::Image,
1,
Unit::Percent,
);
assert!(matches!(
body,
Body::Text(d) if d.value == "19.1% comments · 210 / 1,100 lines",
));
}
#[test]
fn format_value_kloc_scales_thousands_and_millions() {
assert_eq!(format_value(&lang_stat(0, 999), Unit::Kloc), "999");
assert_eq!(format_value(&lang_stat(0, 1_234), Unit::Kloc), "1.2k");
assert_eq!(format_value(&lang_stat(0, 1_234_567), Unit::Kloc), "1.2M");
}
#[test]
fn parse_options_defaults_when_missing() {
let opts = parse_options(None).unwrap();
assert!(opts.limit.is_none());
assert!(opts.unit.is_none());
}
#[tokio::test]
async fn fetch_rejects_invalid_options() {
let err = CodeComments
.fetch(&ctx(None, Some(r#"bogus = true"#)))
.await
.unwrap_err();
assert!(err.to_string().contains("invalid options"));
}
#[test]
fn fetch_scans_cwd_repo_for_multiple_shapes() {
let _lock = crate::paths::TEST_ENV_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let (_tmp, repo) = crate::fetcher::git::test_support::make_repo();
crate::fetcher::git::test_support::commit_touching(
&repo,
"src/lib.rs",
"// a code comment\nfn main() {}\n",
);
let workdir = repo.workdir().unwrap().to_path_buf();
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let prev_cwd = std::env::current_dir().unwrap();
std::env::set_current_dir(&workdir).unwrap();
let text = rt.block_on(CodeComments.fetch(&ctx(Some(Shape::Text), None)));
let badge = rt.block_on(CodeComments.fetch(&ctx(Some(Shape::Badge), Some(r#"limit = 1"#))));
let entries = rt.block_on(CodeComments.fetch(&ctx(
Some(Shape::Entries),
Some(
r#"
limit = 1
unit = "kloc"
"#,
),
)));
std::env::set_current_dir(prev_cwd).unwrap();
assert!(matches!(
text.unwrap().body,
Body::Text(d) if d.value.contains("comments"),
));
assert!(matches!(
badge.unwrap().body,
Body::Badge(d) if !d.label.is_empty(),
));
assert!(matches!(
entries.unwrap().body,
Body::Entries(d)
if d.items.len() == 1
&& !d.items[0].key.is_empty()
&& !d.items[0].value.as_deref().unwrap_or_default().is_empty(),
));
}
}