loc-rs 0.2.7

Advanced Lines of Code counter with function extraction, git integration, and parallel processing
// tests/core.rs — Testing core line counting logic and binary filtering

mod common;
use common::{make_fixture, run_loc};

#[test]
fn test_total_lines_reported() {
    let fixture = make_fixture(&[
        ("a.py", "x = 1\ny = 2\nz = 3\n"),          // 3 lines
        ("b.py", "print('hello')\nprint('bye')\n"), // 2 lines
    ]);

    let out = run_loc(&[fixture.path().to_str().unwrap()]);
    let stdout = String::from_utf8_lossy(&out.stdout);
    assert!(
        stdout.contains('5') || stdout.contains("5"),
        "Expected total of 5 lines in output:\n{}",
        stdout
    );
}

#[test]
fn test_binary_files_skipped_in_line_count() {
    // A file with null bytes is binary
    let fixture = make_fixture(&[("image.png", "\x00\x00\x00\x00binary\x00")]);

    let out = run_loc(&[fixture.path().to_str().unwrap()]);
    assert!(out.status.success());
    let stdout = String::from_utf8_lossy(&out.stdout);
    // Binary files count as 0 lines toward the total
    assert!(
        stdout.contains('0') || !stdout.contains("image.png"),
        "Binary file should not contribute lines"
    );
}

#[test]
fn test_empty_file() {
    let fixture = make_fixture(&[("empty.txt", "")]);
    let out = run_loc(&[fixture.path().to_str().unwrap()]);
    assert!(out.status.success());
    let stdout = String::from_utf8_lossy(&out.stdout);
    assert!(stdout.contains('0'), "Empty file should show 0 lines");
}

#[test]
fn test_no_trailing_newline_integration() {
    let fixture = make_fixture(&[("no_newline.txt", "line1\nline2")]);
    let out = run_loc(&[fixture.path().to_str().unwrap()]);
    assert!(out.status.success());
    let stdout = String::from_utf8_lossy(&out.stdout);
    // Should be 2 lines
    assert!(
        stdout.contains('2'),
        "Expected 2 lines for no trailing newline file"
    );
}

// ── Lockfile tests ───────────────────────────────────────────────────────────

#[test]
fn test_lockfile_lines_excluded_from_stats() {
    let fixture = make_fixture(&[
        ("main.rs", "fn main() {}\n"),
        // Cargo.lock has 4 lines — none should appear in totals
        (
            "Cargo.lock",
            "# This file is automatically @generated by Cargo.\nversion = 3\n\n[[package]]\n",
        ),
    ]);

    let out = run_loc(&[fixture.path().to_str().unwrap()]);
    assert!(out.status.success());
    let stdout = String::from_utf8_lossy(&out.stdout);
    // Only main.rs (1 line) should count; Cargo.lock (4 lines) must be excluded
    assert!(
        stdout.contains('1'),
        "Expected 1 line total (lockfile excluded):\n{}",
        stdout
    );
}

#[test]
fn test_lockfile_appears_in_tree_with_tag() {
    let fixture = make_fixture(&[
        ("main.rs", "fn main() {}\n"),
        ("Cargo.lock", "version = 3\n"),
    ]);

    let out = run_loc(&["--tree", fixture.path().to_str().unwrap()]);
    assert!(out.status.success());
    let stdout = String::from_utf8_lossy(&out.stdout);
    assert!(
        stdout.contains("Cargo.lock"),
        "Cargo.lock should appear in tree:\n{}",
        stdout
    );
    assert!(
        stdout.contains("[lockfile]"),
        "Cargo.lock should carry the [lockfile] tag:\n{}",
        stdout
    );
}

#[test]
fn test_lockfile_has_no_line_count_in_tree() {
    let fixture = make_fixture(&[
        ("main.rs", "fn main() {}\n"),
        // 5 lines — the count must not appear next to the lockfile entry
        (
            "yarn.lock",
            "# yarn lockfile v1\n\nsome-pkg@1.0.0:\n  version \"1.0.0\"\n  resolved \"\"\n",
        ),
    ]);

    let out = run_loc(&["--tree", fixture.path().to_str().unwrap()]);
    assert!(out.status.success());
    let stdout = String::from_utf8_lossy(&out.stdout);
    // The tree for yarn.lock should NOT contain "(5)"
    assert!(
        !stdout.contains("(5)"),
        "Lockfile should not show a line count in the tree:\n{}",
        stdout
    );
}

#[test]
fn test_lockfile_count_shown_in_summary() {
    let fixture = make_fixture(&[
        ("main.rs", "fn main() {}\n"),
        ("Cargo.lock", "version = 3\n"),
        ("yarn.lock", "# yarn lockfile v1\n"),
    ]);

    let out = run_loc(&[fixture.path().to_str().unwrap()]);
    assert!(out.status.success());
    let stdout = String::from_utf8_lossy(&out.stdout);
    assert!(
        stdout.contains("Lockfiles"),
        "Summary should mention lockfile count:\n{}",
        stdout
    );
    assert!(
        stdout.contains('2'),
        "Lockfile count should be 2:\n{}",
        stdout
    );
}

#[test]
fn test_multiple_lockfile_ecosystems() {
    // Verify a cross-ecosystem set is handled consistently
    let fixture = make_fixture(&[
        ("app.py", "print('hi')\n"),
        ("Cargo.lock", "version = 3\n"),
        ("package-lock.json", "{}\n"),
        ("poetry.lock", "# poetry\n"),
        ("go.sum", "module hash\n"),
    ]);

    let out = run_loc(&[fixture.path().to_str().unwrap()]);
    assert!(out.status.success());
    let stdout = String::from_utf8_lossy(&out.stdout);
    // Only app.py (1 line) should count
    assert!(
        stdout.contains('1'),
        "Only source file lines should count, not lockfiles:\n{}",
        stdout
    );
}