ciforge-cli 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. {ciforge_cli-0.1.0 → ciforge_cli-0.2.0}/PKG-INFO +6 -3
  2. {ciforge_cli-0.1.0 → ciforge_cli-0.2.0}/README.md +5 -2
  3. {ciforge_cli-0.1.0 → ciforge_cli-0.2.0}/pyproject.toml +1 -1
  4. ciforge_cli-0.2.0/src/ciforge/ai_reviewer.py +50 -0
  5. ciforge_cli-0.2.0/src/ciforge/assets.py +25 -0
  6. {ciforge_cli-0.1.0 → ciforge_cli-0.2.0}/src/ciforge/cli.py +5 -1
  7. ciforge_cli-0.2.0/src/ciforge/code_quality.py +64 -0
  8. ciforge_cli-0.2.0/src/ciforge/l10n.py +39 -0
  9. ciforge_cli-0.2.0/src/ciforge/metrics.py +26 -0
  10. {ciforge_cli-0.1.0 → ciforge_cli-0.2.0}/src/ciforge_cli.egg-info/PKG-INFO +6 -3
  11. {ciforge_cli-0.1.0 → ciforge_cli-0.2.0}/src/ciforge_cli.egg-info/SOURCES.txt +4 -0
  12. ciforge_cli-0.2.0/tests/test_ciforge.py +122 -0
  13. ciforge_cli-0.1.0/src/ciforge/code_quality.py +0 -36
  14. ciforge_cli-0.1.0/tests/test_ciforge.py +0 -70
  15. {ciforge_cli-0.1.0 → ciforge_cli-0.2.0}/LICENSE +0 -0
  16. {ciforge_cli-0.1.0 → ciforge_cli-0.2.0}/setup.cfg +0 -0
  17. {ciforge_cli-0.1.0 → ciforge_cli-0.2.0}/src/ciforge/__init__.py +0 -0
  18. {ciforge_cli-0.1.0 → ciforge_cli-0.2.0}/src/ciforge/config_validator.py +0 -0
  19. {ciforge_cli-0.1.0 → ciforge_cli-0.2.0}/src/ciforge/coverage.py +0 -0
  20. {ciforge_cli-0.1.0 → ciforge_cli-0.2.0}/src/ciforge/scanner.py +0 -0
  21. {ciforge_cli-0.1.0 → ciforge_cli-0.2.0}/src/ciforge/secrets.py +0 -0
  22. {ciforge_cli-0.1.0 → ciforge_cli-0.2.0}/src/ciforge_cli.egg-info/dependency_links.txt +0 -0
  23. {ciforge_cli-0.1.0 → ciforge_cli-0.2.0}/src/ciforge_cli.egg-info/entry_points.txt +0 -0
  24. {ciforge_cli-0.1.0 → ciforge_cli-0.2.0}/src/ciforge_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ciforge-cli
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: A zero-dependency CI tool replacing multiple CI services.
5
5
  Keywords: ci,github-actions,code-quality,coverage,linting
6
6
  Classifier: Development Status :: 3 - Alpha
@@ -23,11 +23,14 @@ Dynamic: license-file
23
23
 
24
24
  ## Features
25
25
 
26
- - **Code Quality**: Intelligent linting and complexity analysis without heavy dependencies.
26
+ - **Code Quality**: Intelligent linting and AST-based complexity analysis (cyclomatic complexity, long functions).
27
27
  - **Test Coverage Analysis**: Deep inspection of test coverage gaps.
28
28
  - **Secret Detection**: Catch hardcoded credentials before they reach your repository.
29
29
  - **Config Validation**: Ensure your CI/CD and infrastructure configuration files are sound.
30
- - **PR Metrics**: Analyze pull request size, churn, and impact metrics.
30
+ - **PR Metrics & Velocity Tracking**: Analyze pull request size, churn, and time span metrics.
31
+ - **AI Reviewer**: Integrates with OpenAI to automatically find logic flaws and missing edge cases.
32
+ - **Image Optimization**: Detects large unoptimized assets (.png, .jpg, .jpeg) over 500KB.
33
+ - **Localization Sync**: Finds missing translation keys by comparing localization files against a base en.json.
31
34
 
32
35
  ## Installation
33
36
 
@@ -4,11 +4,14 @@
4
4
 
5
5
  ## Features
6
6
 
7
- - **Code Quality**: Intelligent linting and complexity analysis without heavy dependencies.
7
+ - **Code Quality**: Intelligent linting and AST-based complexity analysis (cyclomatic complexity, long functions).
8
8
  - **Test Coverage Analysis**: Deep inspection of test coverage gaps.
9
9
  - **Secret Detection**: Catch hardcoded credentials before they reach your repository.
10
10
  - **Config Validation**: Ensure your CI/CD and infrastructure configuration files are sound.
11
- - **PR Metrics**: Analyze pull request size, churn, and impact metrics.
11
+ - **PR Metrics & Velocity Tracking**: Analyze pull request size, churn, and time span metrics.
12
+ - **AI Reviewer**: Integrates with OpenAI to automatically find logic flaws and missing edge cases.
13
+ - **Image Optimization**: Detects large unoptimized assets (.png, .jpg, .jpeg) over 500KB.
14
+ - **Localization Sync**: Finds missing translation keys by comparing localization files against a base en.json.
12
15
 
13
16
  ## Installation
14
17
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ciforge-cli"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "A zero-dependency CI tool replacing multiple CI services."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -0,0 +1,50 @@
1
+ import os
2
+ import json
3
+ import urllib.request
4
+ from typing import List
5
+ from .scanner import Finding, git_changed_files, git_diff
6
+
7
+ def analyze() -> List[Finding]:
8
+ api_key = os.environ.get("CIFORGE_AI_KEY")
9
+ if not api_key:
10
+ return []
11
+
12
+ files = git_changed_files()
13
+ diffs = []
14
+ for f in files:
15
+ diff_text = git_diff(f)
16
+ if diff_text:
17
+ diffs.append(f"File: {f}\n{diff_text}")
18
+
19
+ if not diffs:
20
+ return []
21
+
22
+ full_diff = "\n\n".join(diffs)
23
+
24
+ # We don't want to send huge diffs to API, but for this mock/simple implementation we do.
25
+ prompt = "Review the following code diff and identify logic flaws or missing edge cases. Provide a brief summary.\n\n" + full_diff
26
+
27
+ req_data = json.dumps({
28
+ "model": "gpt-3.5-turbo",
29
+ "messages": [
30
+ {"role": "system", "content": "You are a code reviewer."},
31
+ {"role": "user", "content": prompt}
32
+ ]
33
+ }).encode("utf-8")
34
+
35
+ req = urllib.request.Request(
36
+ "https://api.openai.com/v1/chat/completions",
37
+ data=req_data,
38
+ headers={
39
+ "Content-Type": "application/json",
40
+ "Authorization": f"Bearer {api_key}"
41
+ }
42
+ )
43
+
44
+ try:
45
+ with urllib.request.urlopen(req) as response:
46
+ result = json.loads(response.read().decode("utf-8"))
47
+ summary = result["choices"][0]["message"]["content"]
48
+ return [Finding("AI_Review", 0, f"AI Reviewer: {summary}", "medium")]
49
+ except Exception as e:
50
+ return [Finding("AI_Review", 0, f"AI Reviewer failed: {e}", "low")]
@@ -0,0 +1,25 @@
1
+ import os
2
+ from typing import List
3
+ from .scanner import Finding
4
+
5
+ def analyze() -> List[Finding]:
6
+ findings = []
7
+ # Analyze the repository files for large images
8
+ for root, _, files in os.walk('.'):
9
+ if '.git' in root:
10
+ continue
11
+ for file in files:
12
+ if file.lower().endswith(('.png', '.jpg', '.jpeg')):
13
+ file_path = os.path.join(root, file)
14
+ try:
15
+ size = os.path.getsize(file_path)
16
+ if size > 500 * 1024:
17
+ findings.append(Finding(
18
+ file=file_path,
19
+ line=0,
20
+ message=f"Unoptimized asset: {file} is {size / (1024 * 1024):.2f}MB. Compress it.",
21
+ severity="high"
22
+ ))
23
+ except Exception:
24
+ pass
25
+ return findings
@@ -1,6 +1,6 @@
1
1
  import argparse
2
2
  import sys
3
- from . import scanner, code_quality, secrets, config_validator, coverage
3
+ from . import scanner, code_quality, secrets, config_validator, coverage, ai_reviewer, assets, l10n, metrics
4
4
 
5
5
  SEVERITY_LEVELS = {'low': 0, 'medium': 1, 'high': 2, 'critical': 3}
6
6
 
@@ -19,6 +19,10 @@ def main():
19
19
  all_findings.extend(config_validator.analyze(f, diff_text))
20
20
 
21
21
  all_findings.extend(coverage.analyze())
22
+ all_findings.extend(ai_reviewer.analyze())
23
+ all_findings.extend(assets.analyze())
24
+ all_findings.extend(l10n.analyze())
25
+ all_findings.extend(metrics.analyze())
22
26
 
23
27
  if not all_findings:
24
28
  print("No issues found. Great job!")
@@ -0,0 +1,64 @@
1
+ import re
2
+ import ast
3
+ import os
4
+ from typing import List
5
+ from .scanner import Finding, _extract_diff_sections, git_diff
6
+
7
+ def analyze(file_path: str, diff_text: str = None) -> List[Finding]:
8
+ if diff_text is None:
9
+ diff_text = git_diff(file_path)
10
+
11
+ findings = []
12
+ added_lines = _extract_diff_sections(diff_text)
13
+
14
+ contiguous_count = 0
15
+ contiguous_start_line = 0
16
+
17
+ for line_num, line_content in added_lines:
18
+ # Check for TODO / FIXME / HACK
19
+ match = re.search(r'\b(TODO|FIXME|HACK)\b', line_content)
20
+ if match:
21
+ findings.append(Finding(file_path, line_num, f"Found {match.group(1)}", "medium"))
22
+
23
+ # Check for console.log, print(), debugger
24
+ if re.search(r'\b(console\.log|print\s*\(|debugger\b)', line_content):
25
+ findings.append(Finding(file_path, line_num, "Found debug statement", "medium"))
26
+
27
+ # Very large function primitive check
28
+ if line_content.strip() == '':
29
+ contiguous_count = 0
30
+ else:
31
+ if contiguous_count == 0:
32
+ contiguous_start_line = line_num
33
+ contiguous_count += 1
34
+
35
+ if contiguous_count == 51:
36
+ findings.append(Finding(file_path, contiguous_start_line, "Very large function/block detected (>50 lines)", "low"))
37
+
38
+ # AST analysis for python files
39
+ if file_path.endswith('.py') and os.path.exists(file_path):
40
+ try:
41
+ with open(file_path, 'r', encoding='utf-8') as f:
42
+ tree = ast.parse(f.read(), filename=file_path)
43
+
44
+ for node in ast.walk(tree):
45
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
46
+ # Cyclomatic complexity
47
+ complexity = 1
48
+ for child in ast.walk(node):
49
+ if isinstance(child, (ast.If, ast.For, ast.While, ast.Try, ast.ExceptHandler, ast.With, ast.AsyncFor, ast.AsyncWith)):
50
+ complexity += 1
51
+ elif isinstance(child, ast.BoolOp):
52
+ complexity += len(child.values) - 1
53
+
54
+ if complexity > 10:
55
+ findings.append(Finding(file_path, getattr(node, 'lineno', 0), f"High cyclomatic complexity: {complexity} (>10)", "high"))
56
+
57
+ # Long function check
58
+ if hasattr(node, 'end_lineno') and hasattr(node, 'lineno'):
59
+ if node.end_lineno - node.lineno > 50:
60
+ findings.append(Finding(file_path, node.lineno, f"Long function: {node.name} is {node.end_lineno - node.lineno} lines (>50)", "high"))
61
+ except Exception:
62
+ pass
63
+
64
+ return findings
@@ -0,0 +1,39 @@
1
+ import os
2
+ import json
3
+ from typing import List
4
+ from .scanner import Finding
5
+
6
+ def analyze() -> List[Finding]:
7
+ findings = []
8
+
9
+ # Scan repo for localization files. Pick en.json as base.
10
+ for root, _, files in os.walk('.'):
11
+ if '.git' in root:
12
+ continue
13
+
14
+ if 'en.json' in files:
15
+ en_path = os.path.join(root, 'en.json')
16
+ try:
17
+ with open(en_path, 'r', encoding='utf-8') as f:
18
+ en_data = json.load(f)
19
+ en_keys = set(en_data.keys())
20
+
21
+ for file in files:
22
+ if file.endswith('.json') and file != 'en.json':
23
+ file_path = os.path.join(root, file)
24
+ with open(file_path, 'r', encoding='utf-8') as f:
25
+ data = json.load(f)
26
+ keys = set(data.keys())
27
+
28
+ missing = en_keys - keys
29
+ for key in missing:
30
+ findings.append(Finding(
31
+ file=file_path,
32
+ line=0,
33
+ message=f"Missing translation key: '{key}'",
34
+ severity="medium"
35
+ ))
36
+ except Exception:
37
+ pass
38
+
39
+ return findings
@@ -0,0 +1,26 @@
1
+ import subprocess
2
+ from typing import List
3
+ from .scanner import Finding
4
+
5
+ def analyze() -> List[Finding]:
6
+ findings = []
7
+ try:
8
+ # Get commit timestamps
9
+ result = subprocess.run(["git", "log", "--format=%ct", "--reverse"], capture_output=True, text=True)
10
+ if result.returncode == 0:
11
+ lines = result.stdout.splitlines()
12
+ if len(lines) >= 2:
13
+ first_commit = int(lines[0])
14
+ last_commit = int(lines[-1])
15
+ diff_seconds = last_commit - first_commit
16
+ hours = diff_seconds / 3600
17
+ findings.append(Finding(
18
+ file="Git Metrics",
19
+ line=0,
20
+ message=f"Velocity Report: Branch active for {hours:.1f} hours over {len(lines)} commits.",
21
+ severity="low"
22
+ ))
23
+ except Exception:
24
+ pass
25
+
26
+ return findings
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ciforge-cli
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: A zero-dependency CI tool replacing multiple CI services.
5
5
  Keywords: ci,github-actions,code-quality,coverage,linting
6
6
  Classifier: Development Status :: 3 - Alpha
@@ -23,11 +23,14 @@ Dynamic: license-file
23
23
 
24
24
  ## Features
25
25
 
26
- - **Code Quality**: Intelligent linting and complexity analysis without heavy dependencies.
26
+ - **Code Quality**: Intelligent linting and AST-based complexity analysis (cyclomatic complexity, long functions).
27
27
  - **Test Coverage Analysis**: Deep inspection of test coverage gaps.
28
28
  - **Secret Detection**: Catch hardcoded credentials before they reach your repository.
29
29
  - **Config Validation**: Ensure your CI/CD and infrastructure configuration files are sound.
30
- - **PR Metrics**: Analyze pull request size, churn, and impact metrics.
30
+ - **PR Metrics & Velocity Tracking**: Analyze pull request size, churn, and time span metrics.
31
+ - **AI Reviewer**: Integrates with OpenAI to automatically find logic flaws and missing edge cases.
32
+ - **Image Optimization**: Detects large unoptimized assets (.png, .jpg, .jpeg) over 500KB.
33
+ - **Localization Sync**: Finds missing translation keys by comparing localization files against a base en.json.
31
34
 
32
35
  ## Installation
33
36
 
@@ -2,10 +2,14 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  src/ciforge/__init__.py
5
+ src/ciforge/ai_reviewer.py
6
+ src/ciforge/assets.py
5
7
  src/ciforge/cli.py
6
8
  src/ciforge/code_quality.py
7
9
  src/ciforge/config_validator.py
8
10
  src/ciforge/coverage.py
11
+ src/ciforge/l10n.py
12
+ src/ciforge/metrics.py
9
13
  src/ciforge/scanner.py
10
14
  src/ciforge/secrets.py
11
15
  src/ciforge_cli.egg-info/PKG-INFO
@@ -0,0 +1,122 @@
1
+ import unittest
2
+ from unittest.mock import patch, mock_open
3
+
4
+ from src.ciforge.scanner import Finding, _extract_diff_sections
5
+ from src.ciforge import code_quality, secrets, config_validator, coverage, ai_reviewer, assets, l10n, metrics
6
+
7
+ class TestCiforge(unittest.TestCase):
8
+
9
+ def test_extract_diff_sections(self):
10
+ diff = """--- a/file.py
11
+ +++ b/file.py
12
+ @@ -10,3 +10,4 @@
13
+ def func():
14
+ - pass
15
+ + print('hello')
16
+ + return True
17
+ """
18
+ added = _extract_diff_sections(diff)
19
+ self.assertEqual(added, [(11, " print('hello')"), (12, " return True")])
20
+
21
+ def test_code_quality(self):
22
+ diff = "@@ -0,0 +1,55 @@\n+def large_func():\n" + "+ pass\n" * 50
23
+ findings = code_quality.analyze("file.py", diff)
24
+ self.assertTrue(any("Very large function" in f.message for f in findings))
25
+ self.assertTrue(any(f.severity == "low" for f in findings))
26
+
27
+ diff = "@@ -0,0 +1,2 @@\n+# TODO: fix this\n+console.log('test')\n"
28
+ findings = code_quality.analyze("file.js", diff)
29
+ self.assertEqual(len(findings), 2)
30
+ self.assertEqual(findings[0].message, "Found TODO")
31
+ self.assertEqual(findings[1].message, "Found debug statement")
32
+
33
+ def test_secrets(self):
34
+ fake_aws = "AKIA" + "IOSFODNN7EXAMPLE"
35
+ diff = f"@@ -0,0 +1,1 @@\n+my_var = '{fake_aws}'\n"
36
+ findings = secrets.analyze("config.py", diff)
37
+ self.assertEqual(len(findings), 1)
38
+ self.assertTrue("aws_key" in findings[0].message)
39
+ self.assertEqual(findings[0].severity, "critical")
40
+
41
+ fake_gh = "ghp_" + "123456789012345678901234567890123456"
42
+ diff2 = f"@@ -0,0 +1,1 @@\n+my_token_var = '{fake_gh}'\n"
43
+ findings2 = secrets.analyze("auth.py", diff2)
44
+ self.assertEqual(len(findings2), 1)
45
+ self.assertTrue("github_token" in findings2[0].message)
46
+
47
+ def test_config_validator_env(self):
48
+ diff = "@@ -0,0 +1,1 @@\n+SECRET=123\n"
49
+ findings = config_validator.analyze(".env", diff)
50
+ self.assertEqual(len(findings), 1)
51
+ self.assertEqual(findings[0].severity, "high")
52
+
53
+ findings2 = config_validator.analyze(".env.example", diff)
54
+ self.assertEqual(len(findings2), 0)
55
+
56
+ def test_config_validator_yaml(self):
57
+ diff = "@@ -0,0 +1,1 @@\n+\tkey: value\n"
58
+ findings = config_validator.analyze("config.yml", diff)
59
+ self.assertEqual(len(findings), 1)
60
+ self.assertTrue("tabs" in findings[0].message)
61
+ self.assertEqual(findings[0].severity, "medium")
62
+
63
+ @patch("os.path.exists", return_value=True)
64
+ def test_coverage(self, mock_exists):
65
+ with patch("builtins.open", mock_open(read_data='{"coverage": 75.0}')):
66
+ findings = coverage.analyze()
67
+ self.assertEqual(len(findings), 1)
68
+ self.assertTrue("Code coverage too low" in findings[0].message)
69
+ self.assertEqual(findings[0].severity, "medium")
70
+
71
+ @patch("os.environ.get", return_value="dummy_key")
72
+ @patch("src.ciforge.ai_reviewer.git_changed_files", return_value=["test.py"])
73
+ @patch("src.ciforge.ai_reviewer.git_diff", return_value="+print('test')")
74
+ @patch("urllib.request.urlopen")
75
+ def test_ai_reviewer(self, mock_urlopen, mock_git_diff, mock_git_files, mock_env):
76
+ from io import BytesIO
77
+ import json
78
+ mock_response = BytesIO(json.dumps({"choices": [{"message": {"content": "Looks good"}}]}).encode("utf-8"))
79
+ mock_urlopen.return_value.__enter__.return_value = mock_response
80
+ findings = ai_reviewer.analyze()
81
+ self.assertEqual(len(findings), 1)
82
+ self.assertTrue("Looks good" in findings[0].message)
83
+
84
+ @patch("os.walk")
85
+ @patch("os.path.getsize", return_value=600 * 1024)
86
+ def test_assets(self, mock_getsize, mock_walk):
87
+ mock_walk.return_value = [(".", [], ["large_image.png"])]
88
+ findings = assets.analyze()
89
+ self.assertEqual(len(findings), 1)
90
+ self.assertTrue("Unoptimized asset" in findings[0].message)
91
+
92
+ @patch("os.walk")
93
+ @patch("builtins.open", new_callable=mock_open)
94
+ def test_l10n(self, mock_open_file, mock_walk):
95
+ mock_walk.return_value = [(".", [], ["en.json", "fr.json"])]
96
+ def side_effect(filename, *args, **kwargs):
97
+ if "en.json" in filename:
98
+ return mock_open(read_data='{"hello": "world", "test": "1"}').return_value
99
+ else:
100
+ return mock_open(read_data='{"hello": "monde"}').return_value
101
+ mock_open_file.side_effect = side_effect
102
+ findings = l10n.analyze()
103
+ self.assertEqual(len(findings), 1)
104
+ self.assertTrue("Missing translation key: 'test'" in findings[0].message)
105
+
106
+ @patch("subprocess.run")
107
+ def test_metrics(self, mock_run):
108
+ mock_run.return_value.returncode = 0
109
+ mock_run.return_value.stdout = "1000\n2000\n5600"
110
+ findings = metrics.analyze()
111
+ self.assertEqual(len(findings), 1)
112
+ self.assertTrue("Velocity Report" in findings[0].message)
113
+
114
+ @patch("os.path.exists", return_value=True)
115
+ def test_code_quality_ast(self, mock_exists):
116
+ python_code = "def complex_func():\n" + "".join([f" if True:\n pass\n" for _ in range(11)])
117
+ with patch("builtins.open", mock_open(read_data=python_code)):
118
+ findings = code_quality.analyze("file.py", "+def complex_func():")
119
+ self.assertTrue(any("High cyclomatic complexity" in f.message for f in findings))
120
+
121
+ if __name__ == '__main__':
122
+ unittest.main()
@@ -1,36 +0,0 @@
1
- import re
2
- from typing import List
3
- from .scanner import Finding, _extract_diff_sections, git_diff
4
-
5
- def analyze(file_path: str, diff_text: str = None) -> List[Finding]:
6
- if diff_text is None:
7
- diff_text = git_diff(file_path)
8
-
9
- findings = []
10
- added_lines = _extract_diff_sections(diff_text)
11
-
12
- contiguous_count = 0
13
- contiguous_start_line = 0
14
-
15
- for line_num, line_content in added_lines:
16
- # Check for TODO / FIXME / HACK
17
- match = re.search(r'\b(TODO|FIXME|HACK)\b', line_content)
18
- if match:
19
- findings.append(Finding(file_path, line_num, f"Found {match.group(1)}", "medium"))
20
-
21
- # Check for console.log, print(), debugger
22
- if re.search(r'\b(console\.log|print\s*\(|debugger\b)', line_content):
23
- findings.append(Finding(file_path, line_num, "Found debug statement", "medium"))
24
-
25
- # Very large function primitive check
26
- if line_content.strip() == '':
27
- contiguous_count = 0
28
- else:
29
- if contiguous_count == 0:
30
- contiguous_start_line = line_num
31
- contiguous_count += 1
32
-
33
- if contiguous_count == 51:
34
- findings.append(Finding(file_path, contiguous_start_line, "Very large function/block detected (>50 lines)", "low"))
35
-
36
- return findings
@@ -1,70 +0,0 @@
1
- import unittest
2
- from unittest.mock import patch, mock_open
3
-
4
- from src.ciforge.scanner import Finding, _extract_diff_sections
5
- from src.ciforge import code_quality, secrets, config_validator, coverage
6
-
7
- class TestCiforge(unittest.TestCase):
8
-
9
- def test_extract_diff_sections(self):
10
- diff = """--- a/file.py
11
- +++ b/file.py
12
- @@ -10,3 +10,4 @@
13
- def func():
14
- - pass
15
- + print('hello')
16
- + return True
17
- """
18
- added = _extract_diff_sections(diff)
19
- self.assertEqual(added, [(11, " print('hello')"), (12, " return True")])
20
-
21
- def test_code_quality(self):
22
- diff = "@@ -0,0 +1,55 @@\n+def large_func():\n" + "+ pass\n" * 50
23
- findings = code_quality.analyze("file.py", diff)
24
- self.assertTrue(any("Very large function" in f.message for f in findings))
25
- self.assertTrue(any(f.severity == "low" for f in findings))
26
-
27
- diff = "@@ -0,0 +1,2 @@\n+# TODO: fix this\n+console.log('test')\n"
28
- findings = code_quality.analyze("file.js", diff)
29
- self.assertEqual(len(findings), 2)
30
- self.assertEqual(findings[0].message, "Found TODO")
31
- self.assertEqual(findings[1].message, "Found debug statement")
32
-
33
- def test_secrets(self):
34
- diff = "@@ -0,0 +1,1 @@\n+my_var = 'AKIAIOSFODNN7EXAMPLE'\n"
35
- findings = secrets.analyze("config.py", diff)
36
- self.assertEqual(len(findings), 1)
37
- self.assertTrue("aws_key" in findings[0].message)
38
- self.assertEqual(findings[0].severity, "critical")
39
-
40
- diff2 = "@@ -0,0 +1,1 @@\n+my_token_var = 'ghp_123456789012345678901234567890123456'\n"
41
- findings2 = secrets.analyze("auth.py", diff2)
42
- self.assertEqual(len(findings2), 1)
43
- self.assertTrue("github_token" in findings2[0].message)
44
-
45
- def test_config_validator_env(self):
46
- diff = "@@ -0,0 +1,1 @@\n+SECRET=123\n"
47
- findings = config_validator.analyze(".env", diff)
48
- self.assertEqual(len(findings), 1)
49
- self.assertEqual(findings[0].severity, "high")
50
-
51
- findings2 = config_validator.analyze(".env.example", diff)
52
- self.assertEqual(len(findings2), 0)
53
-
54
- def test_config_validator_yaml(self):
55
- diff = "@@ -0,0 +1,1 @@\n+\tkey: value\n"
56
- findings = config_validator.analyze("config.yml", diff)
57
- self.assertEqual(len(findings), 1)
58
- self.assertTrue("tabs" in findings[0].message)
59
- self.assertEqual(findings[0].severity, "medium")
60
-
61
- @patch("os.path.exists", return_value=True)
62
- def test_coverage(self, mock_exists):
63
- with patch("builtins.open", mock_open(read_data='{"coverage": 75.0}')):
64
- findings = coverage.analyze()
65
- self.assertEqual(len(findings), 1)
66
- self.assertTrue("Code coverage too low" in findings[0].message)
67
- self.assertEqual(findings[0].severity, "medium")
68
-
69
- if __name__ == '__main__':
70
- unittest.main()
File without changes
File without changes