Skip to content
Open
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
27 changes: 27 additions & 0 deletions community/Embedded/ZephyrRTOS.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Zephyr RTOS build artifacts
/build/
/twister-out*/

# Generated configuration files
*.config
*.config.old
/zephyr/.config

# Device tree generated files
*.dts.pre.tmp
*.dts_compiled

# Kconfig generated
autoconf.h

# West workspace
/.west/

# Python virtual environment
/.venv/
/venv/

# IDE
.vscode/launch.json
.vscode/tasks.json
*.jlink
78 changes: 78 additions & 0 deletions scripts/strip_trailing_whitespace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""
Strip trailing whitespace from .gitignore template files.

This script fixes trailing whitespace issues in gitignore templates,
which can cause unexpected behavior in pattern matching.
"""

import argparse
import os
import sys


def strip_file(filepath: str, dry_run: bool = False) -> int:
"""Strip trailing whitespace from a file. Returns number of lines fixed."""
with open(filepath, "r", encoding="utf-8", errors="replace") as f:
lines = f.readlines()

fixed_count = 0
new_lines = []
for line in lines:
stripped = line.rstrip() + "\n"
if stripped != line:
fixed_count += 1
new_lines.append(stripped)

# Ensure file ends with a single newline
if new_lines and new_lines[-1] == "\n":
pass # already ends properly
elif not new_lines:
new_lines = ["\n"]

if fixed_count > 0 and not dry_run:
with open(filepath, "w", encoding="utf-8", newline="") as f:
f.writelines(new_lines)

return fixed_count


def main():
parser = argparse.ArgumentParser(
description="Strip trailing whitespace from .gitignore templates."
)
parser.add_argument("files", nargs="*", help="Files to process.")
parser.add_argument("--repo-root", default=".", help="Repo root directory.")
parser.add_argument(
"--dry-run", action="store_true", help="Report but do not modify files."
)
args = parser.parse_args()

if args.files:
targets = args.files
else:
targets = []
for entry in os.listdir(args.repo_root):
full = os.path.join(args.repo_root, entry)
if os.path.isfile(full) and entry.endswith(".gitignore"):
targets.append(full)

total_fixed = 0
for filepath in targets:
count = strip_file(filepath, dry_run=args.dry_run)
if count > 0:
action = "would fix" if args.dry_run else "fixed"
print(f" {action} {count} line(s) in {filepath}")
total_fixed += count

if total_fixed == 0:
print("No trailing whitespace found.")
else:
action = "Would fix" if args.dry_run else "Fixed"
print(f"\n{action} {total_fixed} line(s) total.")

sys.exit(0)


if __name__ == "__main__":
main()
189 changes: 189 additions & 0 deletions scripts/test_validate_templates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
#!/usr/bin/env python3
"""Tests for the gitignore template validator."""

import os
import tempfile
import pytest

from validate_templates import (
Issue,
Severity,
ValidationReport,
analyze_coverage,
check_trailing_whitespace,
discover_templates,
find_conflicts,
find_duplicates,
format_report,
is_valid_glob_pattern,
validate_template,
)


class TestIsValidGlobPattern:
def test_simple_extension(self):
assert is_valid_glob_pattern("*.py") is True

def test_directory_pattern(self):
assert is_valid_glob_pattern("build/") is True

def test_negation(self):
assert is_valid_glob_pattern("!important.txt") is True

def test_double_star(self):
assert is_valid_glob_pattern("**/logs") is True

def test_bracket_pattern(self):
assert is_valid_glob_pattern("*.[oa]") is True

def test_unmatched_bracket(self):
assert is_valid_glob_pattern("*.[o") is False

def test_empty_after_strip(self):
assert is_valid_glob_pattern("!") is False

def test_rooted_pattern(self):
assert is_valid_glob_pattern("/build") is True


class TestCheckTrailingWhitespace:
def test_no_trailing(self):
assert check_trailing_whitespace("*.py", 1) is None

def test_with_trailing_space(self):
issue = check_trailing_whitespace("*.py ", 5)
assert issue is not None
assert issue.severity == Severity.WARNING
assert issue.line_number == 5

def test_blank_line_ignored(self):
assert check_trailing_whitespace(" ", 1) is None


class TestFindDuplicates:
def test_no_duplicates(self):
patterns = [(1, "*.py"), (2, "*.js"), (3, "build/")]
assert find_duplicates(patterns) == []

def test_exact_duplicate(self):
patterns = [(1, "*.py"), (2, "*.js"), (3, "*.py")]
issues = find_duplicates(patterns)
assert len(issues) == 1
assert issues[0].line_number == 3

def test_trailing_slash_normalization(self):
patterns = [(1, "build"), (2, "build/")]
issues = find_duplicates(patterns)
assert len(issues) == 1


class TestFindConflicts:
def test_no_conflicts(self):
patterns = [(1, "*.log"), (2, "*.tmp")]
assert find_conflicts(patterns) == []

def test_include_then_negate(self):
patterns = [(1, "*.log"), (2, "!*.log")]
issues = find_conflicts(patterns)
assert len(issues) == 1
assert issues[0].severity == Severity.WARNING

def test_negate_without_include(self):
patterns = [(1, "!important.txt")]
issues = find_conflicts(patterns)
assert len(issues) == 0


class TestAnalyzeCoverage:
def test_many_wildcards(self):
patterns = [(i, f"*.ext{i}") for i in range(12)]
issues = analyze_coverage(patterns)
msgs = [i.message for i in issues]
assert any("wildcard" in m for m in msgs)

def test_no_directory_patterns(self):
patterns = [(i, f"*.ext{i}") for i in range(6)]
issues = analyze_coverage(patterns)
msgs = [i.message for i in issues]
assert any("directory" in m.lower() for m in msgs)

def test_duplicate_extension_suggestion(self):
patterns = [(1, "*.py"), (2, "*.py")]
issues = analyze_coverage(patterns)
msgs = [i.message for i in issues]
assert any(".py" in m for m in msgs)


class TestValidateTemplate:
def test_valid_template(self):
with tempfile.NamedTemporaryFile(
mode="w", suffix=".gitignore", delete=False
) as f:
f.write("# Build output\nbuild/\n*.o\n*.a\n")
f.flush()
report = validate_template(f.name)
os.unlink(f.name)
assert report.error_count == 0
assert report.pattern_count == 3
assert report.comment_count == 1

def test_invalid_pattern(self):
with tempfile.NamedTemporaryFile(
mode="w", suffix=".gitignore", delete=False
) as f:
f.write("*.[invalid\n")
f.flush()
report = validate_template(f.name)
os.unlink(f.name)
assert report.error_count > 0

def test_file_not_found(self):
report = validate_template("/nonexistent/file.gitignore")
assert report.error_count > 0


class TestFormatReport:
def test_clean_report(self):
report = ValidationReport(
template_file="test.gitignore",
total_lines=5,
pattern_count=3,
comment_count=1,
blank_count=1,
)
output = format_report(report)
assert "No issues found" in output

def test_report_with_issues(self):
report = ValidationReport(
template_file="test.gitignore",
total_lines=5,
pattern_count=3,
)
report.issues.append(
Issue(
severity=Severity.ERROR,
line_number=2,
line_content="*.[bad",
message="Invalid glob pattern",
)
)
output = format_report(report, verbose=True)
assert "ERROR" in output
assert "*.[bad" in output


class TestDiscoverTemplates:
def test_discover_in_directory(self):
with tempfile.TemporaryDirectory() as tmpdir:
# Create fake gitignore files
for name in ["Python.gitignore", "Java.gitignore", "README.md"]:
with open(os.path.join(tmpdir, name), "w") as f:
f.write("# test\n")
templates = discover_templates(tmpdir)
assert len(templates) == 2
assert all(t.endswith(".gitignore") for t in templates)


if __name__ == "__main__":
pytest.main([__file__, "-v"])
Loading