diff --git a/.github/workflows/it-test.yml b/.github/workflows/it-test.yml
index 4f80cee5..10c3140b 100644
--- a/.github/workflows/it-test.yml
+++ b/.github/workflows/it-test.yml
@@ -7,11 +7,32 @@ on:
push:
jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # Needed for git history
+ - name: Set up Go
+ uses: actions/setup-go@v4
+ - name: Build CLI for all platforms
+ run: make build-all
+ - name: Upload CLI binaries
+ uses: actions/upload-artifact@v4
+ with:
+ name: cli-binaries
+ path: |
+ cli-v2-linux
+ cli-v2.exe
+ cli-v2-macos
+
test:
+ needs: build
runs-on: ${{ matrix.os }}
strategy:
matrix:
- os: [ubuntu-latest, macos-latest] # [windows-latest] removed for now
+ os: [ubuntu-latest, macos-latest, windows-latest]
fail-fast: false
steps:
- name: Checkout code
@@ -25,10 +46,9 @@ jobs:
go-version: '1.21'
cache: true
- - name: Download CLI binaries from go workflow
- uses: dawidd6/action-download-artifact@v2
+ - name: Download CLI binaries
+ uses: actions/download-artifact@v4
with:
- workflow: go.yml
name: cli-binaries
path: .
@@ -48,6 +68,38 @@ jobs:
if: matrix.os != 'windows-latest'
run: chmod +x cli-v2
+ - name: Install yq on Windows
+ if: matrix.os == 'windows-latest'
+ shell: pwsh
+ run: |
+ choco install yq -y
+ Import-Module $env:ChocolateyInstall\helpers\chocolateyProfile.psm1
+ refreshenv
+
+ - name: Run init tests on Windows
+ if: matrix.os == 'windows-latest'
+ shell: pwsh
+ run: |
+ $ErrorActionPreference = "Stop"
+ & ./integration-tests/run.ps1
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error "Integration tests failed with exit code $LASTEXITCODE"
+ exit $LASTEXITCODE
+ }
+ env:
+ CODACY_API_TOKEN: ${{ secrets.CODACY_API_TOKEN }}
+
+ - name: Run init tests on Unix
+ if: matrix.os != 'windows-latest'
+ id: run_init_tests_unix
+ continue-on-error: true
+ shell: bash
+ env:
+ CODACY_API_TOKEN: ${{ secrets.CODACY_API_TOKEN }}
+ run: |
+ chmod +x integration-tests/run.sh
+ ./integration-tests/run.sh
+
- name: Run tool tests
if: matrix.os != 'windows-latest'
id: run_tests
@@ -84,7 +136,7 @@ jobs:
fi
- name: Check test results
- if: steps.run_tests.outcome == 'failure'
+ if: failure()
run: |
- echo "Job failed because some tool tests failed. Please check the logs above for details."
+ echo "Job failed because some tests failed. Please check the logs above for details."
exit 1
\ No newline at end of file
diff --git a/cmd/init.go b/cmd/init.go
index e10471ee..4bcace14 100644
--- a/cmd/init.go
+++ b/cmd/init.go
@@ -75,8 +75,8 @@ var initCmd = &cobra.Command{
if err != nil {
log.Fatal(err)
}
- createGitIgnoreFile()
}
+ createGitIgnoreFile()
fmt.Println()
fmt.Println("✅ Successfully initialized Codacy configuration!")
fmt.Println()
@@ -95,12 +95,7 @@ func createGitIgnoreFile() error {
}
defer gitIgnoreFile.Close()
- content := `# Codacy CLI
-tools-configs/
-.gitignore
-cli-config.yaml
-logs/
-`
+ content := "# Codacy CLI\ntools-configs/\n.gitignore\ncli-config.yaml\nlogs/\n"
if _, err := gitIgnoreFile.WriteString(content); err != nil {
return fmt.Errorf("failed to write to .gitignore file: %w", err)
}
@@ -275,6 +270,8 @@ func buildRepositoryConfigurationFiles(token string) error {
PyLint: "pylint",
PMD: "pmd",
DartAnalyzer: "dartanalyzer",
+ Lizard: "lizard",
+ Semgrep: "semgrep",
}
// Generate languages configuration based on API tools response
@@ -384,45 +381,48 @@ func createToolFileConfigurations(tool tools.Tool, patternConfiguration []domain
if err != nil {
return fmt.Errorf("failed to create Trivy config: %v", err)
}
+ fmt.Println("Trivy configuration created based on Codacy settings")
} else {
err := createDefaultTrivyConfigFile(toolsConfigDir)
if err != nil {
return fmt.Errorf("failed to create default Trivy config: %v", err)
}
}
- fmt.Println("Trivy configuration created based on Codacy settings")
case PMD:
if len(patternConfiguration) > 0 {
err := createPMDConfigFile(patternConfiguration, toolsConfigDir)
if err != nil {
return fmt.Errorf("failed to create PMD config: %v", err)
}
+
+ fmt.Println("PMD configuration created based on Codacy settings")
} else {
err := createDefaultPMDConfigFile(toolsConfigDir)
if err != nil {
return fmt.Errorf("failed to create default PMD config: %v", err)
}
}
- fmt.Println("PMD configuration created based on Codacy settings")
+
case PyLint:
if len(patternConfiguration) > 0 {
err := createPylintConfigFile(patternConfiguration, toolsConfigDir)
if err != nil {
return fmt.Errorf("failed to create Pylint config: %v", err)
}
+ fmt.Println("Pylint configuration created based on Codacy settings")
} else {
err := createDefaultPylintConfigFile(toolsConfigDir)
if err != nil {
return fmt.Errorf("failed to create default Pylint config: %v", err)
}
}
- fmt.Println("Pylint configuration created based on Codacy settings")
case DartAnalyzer:
if len(patternConfiguration) > 0 {
err := createDartAnalyzerConfigFile(patternConfiguration, toolsConfigDir)
if err != nil {
return fmt.Errorf("failed to create Dart Analyzer config: %v", err)
}
+ fmt.Println("Dart configuration created based on Codacy settings")
}
case Semgrep:
if len(patternConfiguration) > 0 {
@@ -430,6 +430,7 @@ func createToolFileConfigurations(tool tools.Tool, patternConfiguration []domain
if err != nil {
return fmt.Errorf("failed to create Semgrep config: %v", err)
}
+ fmt.Println("Semgrep configuration created based on Codacy settings")
}
case Lizard:
createLizardConfigFile(toolsConfigDir, patternConfiguration)
@@ -541,7 +542,6 @@ func createLizardConfigFile(toolsConfigDir string, patternConfiguration []domain
var patterns []domain.PatternDefinition
if len(patternConfiguration) == 0 {
- fmt.Println("Using default Lizard configuration")
var err error
patterns, err = tools.FetchDefaultEnabledPatterns(Lizard)
if err != nil {
diff --git a/integration-tests/init-with-token/expected/.codacy/codacy.yaml b/integration-tests/init-with-token/expected/.codacy/codacy.yaml
new file mode 100644
index 00000000..3fef092a
--- /dev/null
+++ b/integration-tests/init-with-token/expected/.codacy/codacy.yaml
@@ -0,0 +1,10 @@
+runtimes:
+ - node@22.2.0
+ - python@3.11.11
+tools:
+ - semgrep@1.78.0
+ - lizard@1.17.19
+ - eslint@8.57.0
+ - trivy@0.59.1
+ - pylint@3.3.6
+ - pmd@6.55.0
diff --git a/integration-tests/init-with-token/expected/.gitignore b/integration-tests/init-with-token/expected/.gitignore
new file mode 100644
index 00000000..780308b3
--- /dev/null
+++ b/integration-tests/init-with-token/expected/.gitignore
@@ -0,0 +1,5 @@
+# Codacy CLI
+tools-configs/
+.gitignore
+cli-config.yaml
+logs/
diff --git a/integration-tests/init-with-token/expected/cli-config.yaml b/integration-tests/init-with-token/expected/cli-config.yaml
new file mode 100644
index 00000000..644407e1
--- /dev/null
+++ b/integration-tests/init-with-token/expected/cli-config.yaml
@@ -0,0 +1 @@
+mode: remote
\ No newline at end of file
diff --git a/integration-tests/init-with-token/expected/codacy.yaml b/integration-tests/init-with-token/expected/codacy.yaml
new file mode 100644
index 00000000..5da91080
--- /dev/null
+++ b/integration-tests/init-with-token/expected/codacy.yaml
@@ -0,0 +1,10 @@
+runtimes:
+ - node@22.2.0
+ - python@3.11.11
+tools:
+ - eslint@8.57.0
+ - trivy@0.59.1
+ - pylint@3.3.6
+ - pmd@6.55.0
+ - semgrep@1.78.0
+ - lizard@1.17.19
diff --git a/integration-tests/init-with-token/expected/tools-configs/eslint.config.mjs b/integration-tests/init-with-token/expected/tools-configs/eslint.config.mjs
new file mode 100644
index 00000000..b5931786
--- /dev/null
+++ b/integration-tests/init-with-token/expected/tools-configs/eslint.config.mjs
@@ -0,0 +1,6 @@
+export default [
+ {
+ rules: {
+ }
+ }
+];
\ No newline at end of file
diff --git a/integration-tests/init-with-token/expected/tools-configs/languages-config.yaml b/integration-tests/init-with-token/expected/tools-configs/languages-config.yaml
new file mode 100644
index 00000000..9619a373
--- /dev/null
+++ b/integration-tests/init-with-token/expected/tools-configs/languages-config.yaml
@@ -0,0 +1,19 @@
+tools:
+ - name: pylint
+ languages: [Python]
+ extensions: [.py]
+ - name: lizard
+ languages: [Java, JavaScript, Python]
+ extensions: [.java, .js, .jsm, .jsx, .mjs, .py, .vue]
+ - name: pmd
+ languages: [Java, JavaScript]
+ extensions: [.java, .js, .jsm, .jsx, .mjs, .vue]
+ - name: eslint
+ languages: [JavaScript]
+ extensions: [.js, .jsm, .jsx, .mjs, .vue]
+ - name: trivy
+ languages: [Multiple]
+ extensions: []
+ - name: semgrep
+ languages: [Java, JavaScript, JSON, Python]
+ extensions: [.java, .js, .jsm, .json, .jsx, .mjs, .py, .vue]
diff --git a/integration-tests/init-with-token/expected/tools-configs/lizard.yaml b/integration-tests/init-with-token/expected/tools-configs/lizard.yaml
new file mode 100644
index 00000000..87f8b46c
--- /dev/null
+++ b/integration-tests/init-with-token/expected/tools-configs/lizard.yaml
@@ -0,0 +1,40 @@
+patterns:
+ Lizard_ccn-minor:
+ category: Complexity
+ description: Check the Cyclomatic Complexity value of a function or logic block. If the threshold is not met, raise a Minor issue. The default threshold is 5.
+ explanation: |-
+ # Minor Cyclomatic Complexity control
+
+ Check the Cyclomatic Complexity value of a function or logic block. If the threshold is not met, raise a Minor issue. The default threshold is 4.
+ id: Lizard_ccn-minor
+ level: Info
+ severityLevel: Info
+ threshold: 5
+ timeToFix: 5
+ title: Minor Cyclomatic Complexity control
+ Lizard_nloc-critical:
+ category: Complexity
+ description: Check the number of lines of code (without comments) in a function or logic block. If the threshold is not met, raise a Critical issue. The default threshold is 100.
+ explanation: |-
+ # Critical NLOC control - Number of Lines of Code (without comments)
+
+ Check the number of lines of code (without comments) in a function or logic block. If the threshold is not met, raise a Critical issue. The default threshold is 100.
+ id: Lizard_nloc-critical
+ level: Error
+ severityLevel: Error
+ threshold: 100
+ timeToFix: 5
+ title: Critical NLOC control - Number of Lines of Code (without comments)
+ Lizard_nloc-medium:
+ category: Complexity
+ description: Check the number of lines of code (without comments) in a function. If the threshold is not met, raise a Medium issue. The default threshold is 50.
+ explanation: |-
+ # Medium NLOC control - Number of Lines of Code (without comments)
+
+ Check the number of lines of code (without comments) in a function. If the threshold is not met, raise a Medium issue. The default threshold is 50.
+ id: Lizard_nloc-medium
+ level: Warning
+ severityLevel: Warning
+ threshold: 50
+ timeToFix: 5
+ title: Medium NLOC control - Number of Lines of Code (without comments)
diff --git a/integration-tests/init-with-token/expected/tools-configs/pylint.rc b/integration-tests/init-with-token/expected/tools-configs/pylint.rc
new file mode 100644
index 00000000..2047c82e
--- /dev/null
+++ b/integration-tests/init-with-token/expected/tools-configs/pylint.rc
@@ -0,0 +1,9 @@
+[MASTER]
+ignore=CVS
+persistent=yes
+load-plugins=
+
+[MESSAGES CONTROL]
+disable=all
+enable=E1124,E1130,E1133
+
diff --git a/integration-tests/init-with-token/expected/tools-configs/ruleset.xml b/integration-tests/init-with-token/expected/tools-configs/ruleset.xml
new file mode 100644
index 00000000..bcbd424f
--- /dev/null
+++ b/integration-tests/init-with-token/expected/tools-configs/ruleset.xml
@@ -0,0 +1,12 @@
+
+
+ Codacy PMD Ruleset
+
+
+
+
+
+
\ No newline at end of file
diff --git a/integration-tests/init-with-token/expected/tools-configs/semgrep.yaml b/integration-tests/init-with-token/expected/tools-configs/semgrep.yaml
new file mode 100644
index 00000000..f7fdd3f9
--- /dev/null
+++ b/integration-tests/init-with-token/expected/tools-configs/semgrep.yaml
@@ -0,0 +1,69 @@
+rules:
+ - id: apex.lang.security.ncino.endpoints.namedcredentialsconstantmatch.named-credentials-constant-match
+ languages:
+ - apex
+ message: Named Credentials (and callout endpoints) should be used instead of hard-coding credentials. 1. Hard-coded credentials are hard to maintain when mixed in with application code. 2. It is particularly hard to update hard-coded credentials when they are used amongst different classes. 3. Granting a developer access to the codebase means granting knowledge of credentials, and thus keeping a two-level access is not possible. 4. Using different credentials for different environments is troublesome and error-prone.
+ metadata:
+ category: security
+ confidence: HIGH
+ cwe:
+ - 'CWE-540: Inclusion of Sensitive Information in Source Code'
+ impact: HIGH
+ likelihood: LOW
+ references:
+ - https://cwe.mitre.org/data/definitions/540.html
+ subcategory:
+ - vuln
+ technology:
+ - salesforce
+ min-version: 1.44.0
+ mode: taint
+ pattern-sinks:
+ - patterns:
+ - pattern: req.setHeader($X, ...);
+ - focus-metavariable: $X
+ pattern-sources:
+ - pattern: '...String $X = ''Authorization'';'
+ severity: ERROR
+ - id: clojure.lang.security.use-of-md5.use-of-md5
+ languages:
+ - clojure
+ message: MD5 hash algorithm detected. This is not collision resistant and leads to easily-cracked password hashes. Replace with current recommended hashing algorithms.
+ metadata:
+ author: Gabriel Marquet
+ category: security
+ confidence: HIGH
+ cwe:
+ - 'CWE-328: Use of Weak Hash'
+ impact: HIGH
+ likelihood: MEDIUM
+ owasp:
+ - A03:2017 - Sensitive Data Exposure
+ - A02:2021 - Cryptographic Failures
+ references:
+ - https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html
+ - https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
+ source-rule-url: https://github.com/clj-holmes/clj-holmes-rules/blob/main/security/weak-hash-function-md5.yml
+ subcategory:
+ - vuln
+ technology:
+ - clojure
+ pattern-either:
+ - pattern: (MessageDigest/getInstance "MD5")
+ - pattern: (MessageDigest/getInstance MessageDigestAlgorithms/MD5)
+ - pattern: (MessageDigest/getInstance org.apache.commons.codec.digest.MessageDigestAlgorithms/MD5)
+ - pattern: (java.security.MessageDigest/getInstance "MD5")
+ - pattern: (java.security.MessageDigest/getInstance MessageDigestAlgorithms/MD5)
+ - pattern: (java.security.MessageDigest/getInstance org.apache.commons.codec.digest.MessageDigestAlgorithms/MD5)
+ severity: WARNING
+ - id: codacy.generic.plsql.empty-strings
+ languages:
+ - generic
+ message: Empty strings can lead to unexpected behavior and should be handled carefully.
+ metadata:
+ category: security
+ confidence: MEDIUM
+ description: Detects empty strings in the code which might cause issues or bugs.
+ impact: MEDIUM
+ pattern: $VAR VARCHAR2($LENGTH) := '';
+ severity: WARNING
diff --git a/integration-tests/init-with-token/expected/tools-configs/trivy.yaml b/integration-tests/init-with-token/expected/tools-configs/trivy.yaml
new file mode 100644
index 00000000..c785541c
--- /dev/null
+++ b/integration-tests/init-with-token/expected/tools-configs/trivy.yaml
@@ -0,0 +1,10 @@
+severity:
+ - LOW
+ - MEDIUM
+ - HIGH
+ - CRITICAL
+
+scan:
+ scanners:
+ - vuln
+ - secret
diff --git a/integration-tests/init-without-token/expected/.gitignore b/integration-tests/init-without-token/expected/.gitignore
new file mode 100644
index 00000000..780308b3
--- /dev/null
+++ b/integration-tests/init-without-token/expected/.gitignore
@@ -0,0 +1,5 @@
+# Codacy CLI
+tools-configs/
+.gitignore
+cli-config.yaml
+logs/
diff --git a/integration-tests/init-without-token/expected/cli-config.yaml b/integration-tests/init-without-token/expected/cli-config.yaml
new file mode 100644
index 00000000..6ae4b29d
--- /dev/null
+++ b/integration-tests/init-without-token/expected/cli-config.yaml
@@ -0,0 +1 @@
+mode: local
\ No newline at end of file
diff --git a/integration-tests/init-without-token/expected/codacy.yaml b/integration-tests/init-without-token/expected/codacy.yaml
new file mode 100644
index 00000000..a548d1b4
--- /dev/null
+++ b/integration-tests/init-without-token/expected/codacy.yaml
@@ -0,0 +1,12 @@
+runtimes:
+ - node@22.2.0
+ - python@3.11.11
+ - dart@3.7.2
+tools:
+ - eslint@9.3.0
+ - trivy@0.59.1
+ - pylint@3.3.6
+ - pmd@6.55.0
+ - dartanalyzer@3.7.2
+ - semgrep@1.78.0
+ - lizard@1.17.19
diff --git a/integration-tests/init-without-token/expected/tools-configs/lizard.yaml b/integration-tests/init-without-token/expected/tools-configs/lizard.yaml
new file mode 100644
index 00000000..66964fde
--- /dev/null
+++ b/integration-tests/init-without-token/expected/tools-configs/lizard.yaml
@@ -0,0 +1,50 @@
+patterns:
+ Lizard_ccn-medium:
+ category: Complexity
+ description: Check the Cyclomatic Complexity value of a function or logic block. If the threshold is not met, raise a Medium issue. The default threshold is 8.
+ explanation: |-
+ # Medium Cyclomatic Complexity control
+
+ Check the Cyclomatic Complexity value of a function or logic block. If the threshold is not met, raise a Medium issue. The default threshold is 7.
+ id: Lizard_ccn-medium
+ level: Warning
+ severityLevel: Warning
+ threshold: 8
+ timeToFix: 5
+ title: Medium Cyclomatic Complexity control
+ Lizard_file-nloc-medium:
+ category: Complexity
+ description: Check the number of lines of code (without comments) in a file. If the threshold is not met, raise a Medium issue. The default threshold is 500.
+ explanation: ""
+ id: Lizard_file-nloc-medium
+ level: Warning
+ severityLevel: Warning
+ threshold: 500
+ timeToFix: 5
+ title: Medium File NLOC control - Number of Lines of Code (without comments)
+ Lizard_nloc-medium:
+ category: Complexity
+ description: Check the number of lines of code (without comments) in a function. If the threshold is not met, raise a Medium issue. The default threshold is 50.
+ explanation: |-
+ # Medium NLOC control - Number of Lines of Code (without comments)
+
+ Check the number of lines of code (without comments) in a function. If the threshold is not met, raise a Medium issue. The default threshold is 50.
+ id: Lizard_nloc-medium
+ level: Warning
+ severityLevel: Warning
+ threshold: 50
+ timeToFix: 5
+ title: Medium NLOC control - Number of Lines of Code (without comments)
+ Lizard_parameter-count-medium:
+ category: Complexity
+ description: Check the number of parameters sent to a function. If the threshold is not met, raise a Medium issue. The default threshold is 8.
+ explanation: |-
+ # Medium Parameter count control
+
+ Check the number of parameters sent to a function. If the threshold is not met, raise a Medium issue. The default threshold is 5.
+ id: Lizard_parameter-count-medium
+ level: Warning
+ severityLevel: Warning
+ threshold: 8
+ timeToFix: 5
+ title: Medium Parameter count control
diff --git a/integration-tests/run.ps1 b/integration-tests/run.ps1
new file mode 100644
index 00000000..018e84c4
--- /dev/null
+++ b/integration-tests/run.ps1
@@ -0,0 +1,129 @@
+# Stop on first error
+$ErrorActionPreference = "Stop"
+
+# Get the absolute path of the script's directory and CLI path
+$SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path
+$CLI_PATH = Join-Path (Get-Location) "cli-v2.exe"
+
+Write-Host "Script directory: $SCRIPT_DIR"
+Write-Host "Current working directory: $(Get-Location)"
+
+# Check if API token is provided for token-based test
+if (-not $env:CODACY_API_TOKEN) {
+ Write-Host "Warning: CODACY_API_TOKEN environment variable is not set. Token-based test will be skipped."
+}
+
+# Function to normalize and sort configuration values
+function Normalize-Config {
+ param ([string]$file)
+
+ $ext = [System.IO.Path]::GetExtension($file).TrimStart('.')
+
+ switch ($ext) {
+ { $_ -in @('yaml', 'yml') } { yq e '.' $file | Sort-Object }
+ { $_ -in @('rc', 'conf', 'ini', 'xml') } {
+ Get-Content $file | ForEach-Object {
+ if ($_ -match '^[^#].*=.*,') {
+ $parts = $_ -split '='
+ $values = $parts[1] -split ',' | Sort-Object
+ "$($parts[0])=$($values -join ',')"
+ } else { $_ }
+ } | Sort-Object
+ }
+ default { Get-Content $file | Sort-Object }
+ }
+}
+
+function Compare-Files {
+ param (
+ [string]$expectedDir,
+ [string]$actualDir,
+ [string]$label
+ )
+
+ # Compare files
+ Get-ChildItem -Path $expectedDir -File | ForEach-Object {
+ $actualFile = Join-Path $actualDir $_.Name
+ if (-not (Test-Path $actualFile)) {
+ Write-Host "❌ $label/$($_.Name) does not exist in actual output"
+ Write-Host "Expected: $($_.FullName)"
+ Write-Host "Actual should be: $actualFile"
+ exit 1
+ }
+
+ $expectedContent = Normalize-Config $_.FullName
+ $actualContent = Normalize-Config $actualFile
+
+ if (Compare-Object $expectedContent $actualContent) {
+ Write-Host "❌ $label/$($_.Name) does not match expected"
+ Write-Host "=== Expected (normalized) ==="
+ $expectedContent
+ Write-Host "=== Actual (normalized) ==="
+ $actualContent
+ Write-Host "=== Diff ==="
+ Compare-Object $expectedContent $actualContent
+ Write-Host "==================="
+ exit 1
+ }
+ Write-Host "✅ $label/$($_.Name) matches expected"
+ }
+
+ # Compare subdirectories
+ Get-ChildItem -Path $expectedDir -Directory | Where-Object { $_.Name -ne "logs" } | ForEach-Object {
+ $actualSubDir = if ($_.Name -eq ".codacy") { $actualDir } else { Join-Path $actualDir $_.Name }
+
+ if (-not (Test-Path $actualSubDir)) {
+ Write-Host "❌ Directory $label/$($_.Name) does not exist in actual output"
+ Write-Host "Expected: $($_.FullName)"
+ Write-Host "Actual should be: $actualSubDir"
+ exit 1
+ }
+ Compare-Files $_.FullName $actualSubDir "$label/$($_.Name)"
+ }
+}
+
+function Run-InitTest {
+ param (
+ [string]$testDir,
+ [string]$testName,
+ [bool]$useToken
+ )
+
+ Write-Host "Running test: $testName"
+ if (-not (Test-Path $testDir)) {
+ Write-Host "❌ Test directory does not exist: $testDir"
+ exit 1
+ }
+
+ $originalLocation = Get-Location
+ try {
+ Set-Location $testDir
+ if (Test-Path ".codacy") { Remove-Item -Recurse -Force ".codacy" }
+
+ if ($useToken) {
+ if (-not $env:CODACY_API_TOKEN) {
+ Write-Host "❌ Skipping token-based test: CODACY_API_TOKEN not set"
+ return
+ }
+ & $CLI_PATH init --api-token $env:CODACY_API_TOKEN --organization troubleshoot-codacy-dev --provider gh --repository codacy-cli-test
+ } else {
+ & $CLI_PATH init
+ }
+
+ Compare-Files "expected" ".codacy" "Test $testName"
+ Write-Host "✅ Test $testName completed successfully"
+ Write-Host "----------------------------------------"
+ }
+ finally {
+ Set-Location $originalLocation
+ }
+}
+
+# Run tests
+Write-Host "Starting integration tests..."
+Write-Host "----------------------------------------"
+
+Run-InitTest (Join-Path $SCRIPT_DIR "init-without-token") "init-without-token" $false
+Run-InitTest (Join-Path $SCRIPT_DIR "init-with-token") "init-with-token" $true
+
+Write-Host "All tests completed successfully! 🎉"
\ No newline at end of file
diff --git a/integration-tests/run.sh b/integration-tests/run.sh
new file mode 100644
index 00000000..8ff17388
--- /dev/null
+++ b/integration-tests/run.sh
@@ -0,0 +1,133 @@
+#!/bin/bash
+set -e
+
+# Get the absolute path of the script's directory
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
+CLI_PATH="$(pwd)/cli-v2"
+
+echo "Script directory: $SCRIPT_DIR"
+echo "Current working directory: $(pwd)"
+
+# Check if API token is provided for token-based test
+if [ -z "$CODACY_API_TOKEN" ]; then
+ echo "Warning: CODACY_API_TOKEN environment variable is not set. Token-based test will be skipped."
+fi
+
+# Function to normalize and sort configuration values
+normalize_config() {
+ local file=$1
+ local ext="${file##*.}"
+
+ case "$ext" in
+ yaml|yml)
+ # For YAML files, use yq to sort
+ yq e '.' "$file" | sort
+ ;;
+ rc|conf|ini|xml)
+ # For other config files, sort values after '=' and keep other lines
+ awk -F'=' '
+ /^[^#].*=.*,/ {
+ split($2, values, ",")
+ asort(values)
+ printf "%s=", $1
+ for (i=1; i<=length(values); i++) {
+ if (i>1) printf ","
+ printf "%s", values[i]
+ }
+ print ""
+ next
+ }
+ { print }
+ ' "$file" | sort
+ ;;
+ *)
+ # For other files, just sort
+ sort "$file"
+ ;;
+ esac
+}
+
+compare_files() {
+ local expected_dir="$1"
+ local actual_dir="$2"
+ local label="$3"
+
+ # Compare files in current directory
+ for file in "$expected_dir"/*; do
+ [ -f "$file" ] || continue
+ filename=$(basename "$file")
+ actual_file="$actual_dir/$filename"
+
+ if [ ! -f "$actual_file" ]; then
+ echo "❌ $label/$filename does not exist in actual output"
+ echo "Expected: $file"
+ echo "Actual should be: $actual_file"
+ exit 1
+ fi
+
+ if diff <(normalize_config "$file") <(normalize_config "$actual_file") >/dev/null 2>&1; then
+ echo "✅ $label/$filename matches expected"
+ else
+ echo "❌ $label/$filename does not match expected"
+ echo "=== Expected (normalized) ==="
+ normalize_config "$file"
+ echo "=== Actual (normalized) ==="
+ normalize_config "$actual_file"
+ echo "=== Diff ==="
+ diff <(normalize_config "$file") <(normalize_config "$actual_file") || true
+ echo "==================="
+ exit 1
+ fi
+ done
+
+ # Compare subdirectories
+ for dir in "$expected_dir"/*/; do
+ [ -d "$dir" ] || continue
+ dirname=$(basename "$dir")
+ [ "$dirname" = "logs" ] && continue
+
+ if [ ! -d "$actual_dir/$dirname" ]; then
+ echo "❌ Directory $label/$dirname does not exist in actual output"
+ echo "Expected: $dir"
+ echo "Actual should be: $actual_dir/$dirname"
+ exit 1
+ fi
+ compare_files "$dir" "$actual_dir/$dirname" "$label/$dirname"
+ done
+}
+
+run_init_test() {
+ local test_dir="$1"
+ local test_name="$2"
+ local use_token="$3"
+
+ echo "Running test: $test_name"
+ [ -d "$test_dir" ] || { echo "❌ Test directory does not exist: $test_dir"; exit 1; }
+
+ cd "$test_dir" || exit 1
+ rm -rf .codacy
+
+ if [ "$use_token" = "true" ]; then
+ [ -n "$CODACY_API_TOKEN" ] || { echo "❌ Skipping token-based test: CODACY_API_TOKEN not set"; return 0; }
+ "$CLI_PATH" init --api-token "$CODACY_API_TOKEN" --organization troubleshoot-codacy-dev --provider gh --repository codacy-cli-test
+ else
+ "$CLI_PATH" init
+ fi
+
+ compare_files "expected" ".codacy" "Test $test_name"
+ echo "✅ Test $test_name completed successfully"
+ echo "----------------------------------------"
+}
+
+# Run both tests
+echo "Starting integration tests..."
+echo "----------------------------------------"
+
+# Test 1: Init without token
+run_init_test "$SCRIPT_DIR/init-without-token" "init-without-token" "false"
+
+# Test 2: Init with token
+run_init_test "$SCRIPT_DIR/init-with-token" "init-with-token" "true"
+
+echo "All tests completed successfully! 🎉"
+
diff --git a/tools/dartanalyzerRunner_test.go b/tools/dartanalyzerRunner_test.go
deleted file mode 100644
index fb0b8665..00000000
--- a/tools/dartanalyzerRunner_test.go
+++ /dev/null
@@ -1,50 +0,0 @@
-package tools
-
-import (
- "log"
- "os"
- "path/filepath"
- "strings"
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestRunDartAnalyzerToFile(t *testing.T) {
- homeDirectory, err := os.UserHomeDir()
- if err != nil {
- log.Fatal(err.Error())
- }
- currentDirectory, err := os.Getwd()
- if err != nil {
- log.Fatal(err.Error())
- }
- testDirectory := "testdata/repositories/dartanalizer"
- tempResultFile := filepath.Join(os.TempDir(), "eslint.sarif")
- defer os.Remove(tempResultFile)
-
- repositoryToAnalyze := filepath.Join(testDirectory, "src")
- expectedSarifFile := filepath.Join(testDirectory, "expected.sarif")
- dartInstallationDirectory := filepath.Join(homeDirectory, ".cache/codacy/runtimes/dart-sdk")
- dartBinary := "dart"
-
- RunDartAnalyzer(repositoryToAnalyze, dartInstallationDirectory, dartBinary, nil, tempResultFile, "sarif")
-
- expectedSarifBytes, err := os.ReadFile(expectedSarifFile)
- if err != nil {
- log.Fatal(err)
- }
-
- obtainedSarifBytes, err := os.ReadFile(tempResultFile)
- if err != nil {
- log.Fatal(err.Error())
- }
- obtainedSarif := string(obtainedSarifBytes)
-
- filePrefix := currentDirectory + "/"
- actualSarif := strings.ReplaceAll(obtainedSarif, filePrefix, "")
-
- expectedSarif := strings.TrimSpace(string(expectedSarifBytes))
-
- assert.Equal(t, expectedSarif, actualSarif, "output did not match expected")
-}
diff --git a/tools/enigmaRunner_test.go b/tools/enigmaRunner_test.go
deleted file mode 100644
index fbf1ebca..00000000
--- a/tools/enigmaRunner_test.go
+++ /dev/null
@@ -1,57 +0,0 @@
-package tools
-
-import (
- "io/ioutil"
- "os"
- "path/filepath"
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestRunEnigma_NoConfig_NoOutputFile(t *testing.T) {
- tempDir := t.TempDir()
- fakeBinary := "/bin/echo"
- err := RunEnigma(tempDir, tempDir, fakeBinary, []string{"foo.go"}, "", "text")
- assert.NoError(t, err)
-}
-
-func TestRunEnigma_WithConfig_NoOutputFile(t *testing.T) {
- tempDir := t.TempDir()
- configPath := filepath.Join(tempDir, "enigma.yaml")
- ioutil.WriteFile(configPath, []byte("config: true"), 0644)
- fakeBinary := "/bin/echo"
- err := RunEnigma(tempDir, tempDir, fakeBinary, []string{"foo.go"}, "", "text")
- assert.NoError(t, err)
-}
-
-func TestRunEnigma_NoConfig_WithOutputFile(t *testing.T) {
- tempDir := t.TempDir()
- outputFile := filepath.Join(tempDir, "output.txt")
- fakeBinary := "/bin/echo"
- err := RunEnigma(tempDir, tempDir, fakeBinary, []string{"foo.go"}, outputFile, "text")
- assert.NoError(t, err)
- _, err = os.Stat(outputFile)
- assert.NoError(t, err)
-}
-
-func TestRunEnigma_WithConfig_WithOutputFile(t *testing.T) {
- tempDir := t.TempDir()
- configPath := filepath.Join(tempDir, "enigma.yaml")
- ioutil.WriteFile(configPath, []byte("config: true"), 0644)
- outputFile := filepath.Join(tempDir, "output.txt")
- fakeBinary := "/bin/echo"
- err := RunEnigma(tempDir, tempDir, fakeBinary, []string{"foo.go"}, outputFile, "text")
- assert.NoError(t, err)
- _, err = os.Stat(outputFile)
- assert.NoError(t, err)
-}
-
-func TestRunEnigma_CreateOutputFileError(t *testing.T) {
- tempDir := t.TempDir()
- fakeBinary := "/bin/echo"
- // Use a directory as output file to force error
- outputFile := tempDir
- err := RunEnigma(tempDir, tempDir, fakeBinary, []string{"foo.go"}, outputFile, "text")
- assert.Error(t, err)
-}
diff --git a/tools/eslintRunner_test.go b/tools/eslintRunner_test.go
deleted file mode 100644
index a0fd91d0..00000000
--- a/tools/eslintRunner_test.go
+++ /dev/null
@@ -1,49 +0,0 @@
-package tools
-
-import (
- "log"
- "os"
- "path/filepath"
- "strings"
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestRunEslintToFile(t *testing.T) {
- homeDirectory, err := os.UserHomeDir()
- if err != nil {
- log.Fatal(err.Error())
- }
- currentDirectory, err := os.Getwd()
- if err != nil {
- log.Fatal(err.Error())
- }
- testDirectory := "testdata/repositories/test1"
- tempResultFile := filepath.Join(os.TempDir(), "eslint.sarif")
- defer os.Remove(tempResultFile)
-
- repositoryToAnalyze := filepath.Join(testDirectory, "src")
- expectedSarifFile := filepath.Join(testDirectory, "expected.sarif")
- eslintInstallationDirectory := filepath.Join(homeDirectory, ".cache/codacy/tools/eslint@8.57.0")
- nodeBinary := "node"
-
- RunEslint(repositoryToAnalyze, eslintInstallationDirectory, nodeBinary, nil, false, tempResultFile, "sarif")
-
- expectedSarifBytes, err := os.ReadFile(expectedSarifFile)
- if err != nil {
- log.Fatal(err)
- }
-
- obtainedSarifBytes, err := os.ReadFile(tempResultFile)
- if err != nil {
- log.Fatal(err.Error())
- }
- obtainedSarif := string(obtainedSarifBytes)
- filePrefix := "file://" + currentDirectory + "/"
- actualSarif := strings.ReplaceAll(obtainedSarif, filePrefix, "")
-
- expectedSarif := strings.TrimSpace(string(expectedSarifBytes))
-
- assert.Equal(t, expectedSarif, actualSarif, "output did not match expected")
-}
diff --git a/tools/getTools.go b/tools/getTools.go
index 42ee4c26..e9828ace 100644
--- a/tools/getTools.go
+++ b/tools/getTools.go
@@ -135,18 +135,18 @@ type Tool struct {
Name string `json:"name"`
Version string `json:"version"`
Settings struct {
- Enabled bool `json:"isEnabled"`
- UsesConfigFile bool `json:"hasConfigurationFile"`
+ Enabled bool `json:"isEnabled"`
+ HasConfigurationFile bool `json:"hasConfigurationFile"`
+ UsesConfigurationFile bool `json:"usesConfigurationFile"`
} `json:"settings"`
}
// FilterToolsByConfigUsage filters out tools that use their own configuration files
-// Returns only tools that need configuration to be generated for them (UsesConfigFile = false)
+// Returns only tools that need configuration to be generated for them (UsesConfigurationFile = false)
func FilterToolsByConfigUsage(tools []Tool) []Tool {
var filtered []Tool
for _, tool := range tools {
-
- if !tool.Settings.UsesConfigFile {
+ if !tool.Settings.UsesConfigurationFile {
filtered = append(filtered, tool)
} else {
fmt.Printf("Skipping config generation for %s - configured to use repo's config file\n", tool.Name)
diff --git a/tools/getTools_test.go b/tools/getTools_test.go
index 0a38fb8f..07b1558a 100644
--- a/tools/getTools_test.go
+++ b/tools/getTools_test.go
@@ -14,39 +14,45 @@ func TestFilterToolsByConfigUsage(t *testing.T) {
expectedTools []string
}{
{
- name: "tools with UsesConfigFile=true should be filtered out",
+ name: "tools with UsesConfigurationFile=true should be filtered out",
inputTools: []Tool{
{
Uuid: "eslint-uuid",
Name: "eslint",
Settings: struct {
- Enabled bool `json:"isEnabled"`
- UsesConfigFile bool `json:"hasConfigurationFile"`
+ Enabled bool `json:"isEnabled"`
+ HasConfigurationFile bool `json:"hasConfigurationFile"`
+ UsesConfigurationFile bool `json:"usesConfigurationFile"`
}{
- Enabled: true,
- UsesConfigFile: true,
+ Enabled: true,
+ HasConfigurationFile: true,
+ UsesConfigurationFile: true,
},
},
{
Uuid: "trivy-uuid",
Name: "trivy",
Settings: struct {
- Enabled bool `json:"isEnabled"`
- UsesConfigFile bool `json:"hasConfigurationFile"`
+ Enabled bool `json:"isEnabled"`
+ HasConfigurationFile bool `json:"hasConfigurationFile"`
+ UsesConfigurationFile bool `json:"usesConfigurationFile"`
}{
- Enabled: true,
- UsesConfigFile: false,
+ Enabled: true,
+ HasConfigurationFile: false,
+ UsesConfigurationFile: false,
},
},
{
Uuid: "pylint-uuid",
Name: "pylint",
Settings: struct {
- Enabled bool `json:"isEnabled"`
- UsesConfigFile bool `json:"hasConfigurationFile"`
+ Enabled bool `json:"isEnabled"`
+ HasConfigurationFile bool `json:"hasConfigurationFile"`
+ UsesConfigurationFile bool `json:"usesConfigurationFile"`
}{
- Enabled: true,
- UsesConfigFile: false,
+ Enabled: true,
+ HasConfigurationFile: false,
+ UsesConfigurationFile: false,
},
},
},
@@ -60,22 +66,26 @@ func TestFilterToolsByConfigUsage(t *testing.T) {
Uuid: "eslint-uuid",
Name: "eslint",
Settings: struct {
- Enabled bool `json:"isEnabled"`
- UsesConfigFile bool `json:"hasConfigurationFile"`
+ Enabled bool `json:"isEnabled"`
+ HasConfigurationFile bool `json:"hasConfigurationFile"`
+ UsesConfigurationFile bool `json:"usesConfigurationFile"`
}{
- Enabled: true,
- UsesConfigFile: true,
+ Enabled: true,
+ HasConfigurationFile: true,
+ UsesConfigurationFile: true,
},
},
{
Uuid: "trivy-uuid",
Name: "trivy",
Settings: struct {
- Enabled bool `json:"isEnabled"`
- UsesConfigFile bool `json:"hasConfigurationFile"`
+ Enabled bool `json:"isEnabled"`
+ HasConfigurationFile bool `json:"hasConfigurationFile"`
+ UsesConfigurationFile bool `json:"usesConfigurationFile"`
}{
- Enabled: true,
- UsesConfigFile: true,
+ Enabled: true,
+ HasConfigurationFile: true,
+ UsesConfigurationFile: true,
},
},
},
@@ -89,22 +99,26 @@ func TestFilterToolsByConfigUsage(t *testing.T) {
Uuid: "eslint-uuid",
Name: "eslint",
Settings: struct {
- Enabled bool `json:"isEnabled"`
- UsesConfigFile bool `json:"hasConfigurationFile"`
+ Enabled bool `json:"isEnabled"`
+ HasConfigurationFile bool `json:"hasConfigurationFile"`
+ UsesConfigurationFile bool `json:"usesConfigurationFile"`
}{
- Enabled: true,
- UsesConfigFile: false,
+ Enabled: true,
+ HasConfigurationFile: true,
+ UsesConfigurationFile: false,
},
},
{
Uuid: "pylint-uuid",
Name: "pylint",
Settings: struct {
- Enabled bool `json:"isEnabled"`
- UsesConfigFile bool `json:"hasConfigurationFile"`
+ Enabled bool `json:"isEnabled"`
+ HasConfigurationFile bool `json:"hasConfigurationFile"`
+ UsesConfigurationFile bool `json:"usesConfigurationFile"`
}{
- Enabled: true,
- UsesConfigFile: false,
+ Enabled: true,
+ HasConfigurationFile: false,
+ UsesConfigurationFile: false,
},
},
},
@@ -134,10 +148,10 @@ func TestFilterToolsByConfigUsage(t *testing.T) {
assert.True(t, found, "Expected tool %s not found in filtered results", expectedTool)
}
- // Verify no tools with UsesConfigFile=true are in the result
+ // Verify no tools with UsesConfigurationFile=true are in the result
for _, tool := range result {
- assert.False(t, tool.Settings.UsesConfigFile,
- "Tool %s with UsesConfigFile=true should not be in filtered results", tool.Name)
+ assert.False(t, tool.Settings.UsesConfigurationFile,
+ "Tool %s with UsesConfigurationFile=true should not be in filtered results", tool.Name)
}
})
}
diff --git a/tools/language_config.go b/tools/language_config.go
index a1cd802e..4242018f 100644
--- a/tools/language_config.go
+++ b/tools/language_config.go
@@ -64,6 +64,16 @@ func CreateLanguagesConfigFile(apiTools []Tool, toolsConfigDir string, toolIDMap
Languages: []string{"Dart"},
Extensions: []string{".dart"},
},
+ "lizard": {
+ Name: "lizard",
+ Languages: []string{"C", "CPP", "Java", "C#", "JavaScript", "TypeScript", "VueJS", "Objective-C", "Swift", "Python", "Ruby", "TTCN-3", "PHP", "Scala", "GDScript", "Golang", "Lua", "Rust", "Fortran", "Kotlin", "Solidity", "Erlang", "Zig", "Perl"},
+ Extensions: []string{".c", ".cpp", ".cc", ".h", ".hpp", ".java", ".cs", ".js", ".jsx", ".ts", ".tsx", ".vue", ".m", ".swift", ".py", ".rb", ".ttcn", ".php", ".scala", ".gd", ".go", ".lua", ".rs", ".f", ".f90", ".kt", ".sol", ".erl", ".zig", ".pl"},
+ },
+ "semgrep": {
+ Name: "semgrep",
+ Languages: []string{"C", "CPP", "C#", "Generic", "Go", "Java", "JavaScript", "JSON", "Kotlin", "Python", "TypeScript", "Ruby", "Rust", "JSX", "PHP", "Scala", "Swift", "Terraform"},
+ Extensions: []string{".c", ".cpp", ".h", ".hpp", ".cs", ".go", ".java", ".js", ".json", ".kt", ".py", ".ts", ".rb", ".rs", ".jsx", ".php", ".scala", ".swift", ".tf", ".tfvars"},
+ },
}
// Build a list of tool language info for enabled tools
diff --git a/tools/pmdRunner_test.go b/tools/pmdRunner_test.go
deleted file mode 100644
index 30bea013..00000000
--- a/tools/pmdRunner_test.go
+++ /dev/null
@@ -1,126 +0,0 @@
-package tools
-
-import (
- "codacy/cli-v2/config"
- "encoding/json"
- "log"
- "os"
- "path/filepath"
- "strings"
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-// SarifResult represents a single result in a SARIF report
-type SarifResult struct {
- RuleID string `json:"ruleId"`
- Message struct {
- Text string `json:"text"`
- } `json:"message"`
- Locations []struct {
- PhysicalLocation struct {
- ArtifactLocation struct {
- URI string `json:"uri"`
- } `json:"artifactLocation"`
- Region struct {
- StartLine int `json:"startLine"`
- StartColumn int `json:"startColumn"`
- EndLine int `json:"endLine"`
- EndColumn int `json:"endColumn"`
- } `json:"region"`
- } `json:"physicalLocation"`
- } `json:"locations"`
-}
-
-// SarifReport represents the structure of a SARIF report
-type SarifReport struct {
- Runs []struct {
- Results []SarifResult `json:"results"`
- } `json:"runs"`
-}
-
-func TestRunPmdToFile(t *testing.T) {
- homeDirectory, err := os.UserHomeDir()
- if err != nil {
- log.Fatal(err.Error())
- }
- currentDirectory, err := os.Getwd()
- if err != nil {
- log.Fatal(err.Error())
- }
-
- // Use the correct path relative to tools directory
- testDirectory := filepath.Join(currentDirectory, "testdata", "repositories", "pmd")
- repositoryCache := filepath.Join(testDirectory, ".codacy")
- globalCache := filepath.Join(homeDirectory, ".cache/codacy")
-
- tempResultFile := filepath.Join(os.TempDir(), "pmd.sarif")
- defer os.Remove(tempResultFile)
-
- config := *config.NewConfigType(testDirectory, repositoryCache, globalCache)
-
- // Use absolute paths
- repositoryToAnalyze := testDirectory
- // Use the standard ruleset file for testing the PMD runner functionality
- //rulesetFile := filepath.Join(testDirectory, "ruleset.xml")
-
- // Use the same path as defined in plugin.yaml
- pmdBinary := filepath.Join(globalCache, "tools/pmd@6.55.0/pmd-bin-6.55.0/bin/run.sh")
-
- // Run PMD
- err = RunPmd(repositoryToAnalyze, pmdBinary, nil, tempResultFile, "sarif", config)
- if err != nil {
- t.Fatalf("Failed to run pmd: %v", err)
- }
-
- // Check if the output file was created
- obtainedSarifBytes, err := os.ReadFile(tempResultFile)
- if err != nil {
- t.Fatalf("Failed to read output file: %v", err)
- }
-
- // Normalize paths in the obtained SARIF output
- obtainedSarif := string(obtainedSarifBytes)
- obtainedSarif = strings.ReplaceAll(obtainedSarif, currentDirectory+"/", "")
-
- // Parse the normalized SARIF output
- var sarifReport SarifReport
- err = json.Unmarshal([]byte(obtainedSarif), &sarifReport)
- if err != nil {
- t.Fatalf("Failed to parse SARIF output: %v", err)
- }
-
- // Verify we have results
- assert.NotEmpty(t, sarifReport.Runs, "SARIF report should have at least one run")
- assert.NotEmpty(t, sarifReport.Runs[0].Results, "SARIF report should have at least one result")
-
- // Define expected violations
- expectedViolations := map[string]bool{
- "UnusedPrivateField": false,
- "ShortVariable": false,
- "AtLeastOneConstructor": false,
- "CommentRequired": false,
- }
-
- // Check each result
- for _, result := range sarifReport.Runs[0].Results {
- // Mark this rule as found
- expectedViolations[result.RuleID] = true
-
- // Verify the file path is correct
- assert.Contains(t, result.Locations[0].PhysicalLocation.ArtifactLocation.URI, "RulesBreaker.java",
- "Violation should be in RulesBreaker.java")
-
- // Verify line numbers are reasonable
- assert.Greater(t, result.Locations[0].PhysicalLocation.Region.StartLine, 0,
- "Start line should be positive")
- assert.Less(t, result.Locations[0].PhysicalLocation.Region.StartLine, 30,
- "Start line should be within the file")
- }
-
- // Verify all expected violations were found
- for ruleID, found := range expectedViolations {
- assert.True(t, found, "Expected violation %s was not found", ruleID)
- }
-}
diff --git a/tools/semgrepConfigCreator.go b/tools/semgrepConfigCreator.go
index 6272bffe..44c051b6 100644
--- a/tools/semgrepConfigCreator.go
+++ b/tools/semgrepConfigCreator.go
@@ -15,6 +15,10 @@ type semgrepRulesFile struct {
Rules []map[string]interface{} `yaml:"rules"`
}
+// getExecutablePath is a variable that holds the function to get the executable path
+// This is used for testing purposes
+var getExecutablePath = os.Executable
+
// FilterRulesFromFile extracts enabled rules from a rules.yaml file based on configuration
func FilterRulesFromFile(rulesFilePath string, config []domain.PatternConfiguration) ([]byte, error) {
// Read the rules.yaml file
@@ -65,8 +69,15 @@ func FilterRulesFromFile(rulesFilePath string, config []domain.PatternConfigurat
// GetSemgrepConfig gets the Semgrep configuration based on the pattern configuration
func GetSemgrepConfig(config []domain.PatternConfiguration) ([]byte, error) {
- // Get the default rules file location
- rulesFile := filepath.Join("plugins", "tools", "semgrep", "rules.yaml")
+ // Get the executable's directory
+ execPath, err := getExecutablePath()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get executable path: %w", err)
+ }
+ execDir := filepath.Dir(execPath)
+
+ // Get the default rules file location relative to the executable
+ rulesFile := filepath.Join(execDir, "plugins", "tools", "semgrep", "rules.yaml")
// Check if it exists and config is not empty
if _, err := os.Stat(rulesFile); err == nil && len(config) > 0 {
@@ -80,8 +91,15 @@ func GetSemgrepConfig(config []domain.PatternConfiguration) ([]byte, error) {
// GetDefaultSemgrepConfig gets the default Semgrep configuration
func GetDefaultSemgrepConfig() ([]byte, error) {
- // Get the default rules file location
- rulesFile := filepath.Join("plugins", "tools", "semgrep", "rules.yaml")
+ // Get the executable's directory
+ execPath, err := getExecutablePath()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get executable path: %w", err)
+ }
+ execDir := filepath.Dir(execPath)
+
+ // Get the default rules file location relative to the executable
+ rulesFile := filepath.Join(execDir, "plugins", "tools", "semgrep", "rules.yaml")
// If the file exists, return its contents
if _, err := os.Stat(rulesFile); err == nil {
diff --git a/tools/semgrepConfigCreator_test.go b/tools/semgrepConfigCreator_test.go
index b22cb54a..992fc413 100644
--- a/tools/semgrepConfigCreator_test.go
+++ b/tools/semgrepConfigCreator_test.go
@@ -101,41 +101,30 @@ func TestFilterRulesFromFile(t *testing.T) {
// TestGetSemgrepConfig tests the GetSemgrepConfig function
func TestGetSemgrepConfig(t *testing.T) {
- // Override the function to find rules.yaml to use our test file
- originalRulesFilePath := filepath.Join("plugins", "tools", "semgrep", "rules.yaml")
-
// Create a temporary rules file
tempDir := t.TempDir()
testRulesFile := filepath.Join(tempDir, "rules.yaml")
err := os.WriteFile(testRulesFile, []byte(sampleRulesYAML), 0644)
assert.NoError(t, err)
- // Create a backup of the original file if it exists
- backupFilePath := ""
- if _, err := os.Stat(originalRulesFilePath); err == nil {
- backupFilePath = originalRulesFilePath + ".bak"
- err = os.Rename(originalRulesFilePath, backupFilePath)
- assert.NoError(t, err)
+ // Create a mock executable path that points to our temp directory
+ originalGetExecutablePath := getExecutablePath
+ getExecutablePath = func() (string, error) {
+ return filepath.Join(tempDir, "test-executable"), nil
}
+ defer func() {
+ getExecutablePath = originalGetExecutablePath
+ }()
- // Ensure the directory exists
- err = os.MkdirAll(filepath.Dir(originalRulesFilePath), 0755)
+ // Create the plugins directory structure
+ pluginsDir := filepath.Join(tempDir, "plugins", "tools", "semgrep")
+ err = os.MkdirAll(pluginsDir, 0755)
assert.NoError(t, err)
- // Copy our test file to the location
- testFileContent, err := os.ReadFile(testRulesFile)
- assert.NoError(t, err)
- err = os.WriteFile(originalRulesFilePath, testFileContent, 0644)
+ // Copy our test file to the plugins directory
+ err = os.WriteFile(filepath.Join(pluginsDir, "rules.yaml"), []byte(sampleRulesYAML), 0644)
assert.NoError(t, err)
- // Clean up after the test
- defer func() {
- os.Remove(originalRulesFilePath)
- if backupFilePath != "" {
- os.Rename(backupFilePath, originalRulesFilePath)
- }
- }()
-
// Test with valid configuration
config := []domain.PatternConfiguration{
{
@@ -162,41 +151,30 @@ func TestGetSemgrepConfig(t *testing.T) {
// TestGetDefaultSemgrepConfig tests the GetDefaultSemgrepConfig function
func TestGetDefaultSemgrepConfig(t *testing.T) {
- // Override the function to find rules.yaml to use our test file
- originalRulesFilePath := filepath.Join("plugins", "tools", "semgrep", "rules.yaml")
-
// Create a temporary rules file
tempDir := t.TempDir()
testRulesFile := filepath.Join(tempDir, "rules.yaml")
err := os.WriteFile(testRulesFile, []byte(sampleRulesYAML), 0644)
assert.NoError(t, err)
- // Create a backup of the original file if it exists
- backupFilePath := ""
- if _, err := os.Stat(originalRulesFilePath); err == nil {
- backupFilePath = originalRulesFilePath + ".bak"
- err = os.Rename(originalRulesFilePath, backupFilePath)
- assert.NoError(t, err)
+ // Create a mock executable path that points to our temp directory
+ originalGetExecutablePath := getExecutablePath
+ getExecutablePath = func() (string, error) {
+ return filepath.Join(tempDir, "test-executable"), nil
}
+ defer func() {
+ getExecutablePath = originalGetExecutablePath
+ }()
- // Ensure the directory exists
- err = os.MkdirAll(filepath.Dir(originalRulesFilePath), 0755)
+ // Create the plugins directory structure
+ pluginsDir := filepath.Join(tempDir, "plugins", "tools", "semgrep")
+ err = os.MkdirAll(pluginsDir, 0755)
assert.NoError(t, err)
- // Copy our test file to the location
- testFileContent, err := os.ReadFile(testRulesFile)
- assert.NoError(t, err)
- err = os.WriteFile(originalRulesFilePath, testFileContent, 0644)
+ // Copy our test file to the plugins directory
+ err = os.WriteFile(filepath.Join(pluginsDir, "rules.yaml"), []byte(sampleRulesYAML), 0644)
assert.NoError(t, err)
- // Clean up after the test
- defer func() {
- os.Remove(originalRulesFilePath)
- if backupFilePath != "" {
- os.Rename(backupFilePath, originalRulesFilePath)
- }
- }()
-
// Test getting default config
result, err := GetDefaultSemgrepConfig()
assert.NoError(t, err)
@@ -207,7 +185,7 @@ func TestGetDefaultSemgrepConfig(t *testing.T) {
assert.Equal(t, 3, len(parsedRules.Rules))
// Test when rules.yaml doesn't exist
- os.Remove(originalRulesFilePath)
+ os.Remove(filepath.Join(pluginsDir, "rules.yaml"))
_, err = GetDefaultSemgrepConfig()
assert.Error(t, err)
assert.Contains(t, err.Error(), "rules.yaml not found")
diff --git a/tools/semgrepRunner_test.go b/tools/semgrepRunner_test.go
deleted file mode 100644
index 1d81344f..00000000
--- a/tools/semgrepRunner_test.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package tools
-
-import (
- "codacy/cli-v2/plugins"
- "os"
- "path/filepath"
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestRunSemgrepWithSpecificFiles(t *testing.T) {
- homeDirectory, err := os.UserHomeDir()
- if err != nil {
- t.Fatalf("Failed to get home directory: %v", err)
- }
- currentDirectory, err := os.Getwd()
- if err != nil {
- t.Fatalf("Failed to get current directory: %v", err)
- }
-
- // Set up test directories and files
- testDirectory := filepath.Join(currentDirectory, "testdata", "repositories", "semgrep")
- tempResultFile := filepath.Join(os.TempDir(), "semgrep-specific.sarif")
- defer os.Remove(tempResultFile)
-
- // Create tool info for semgrep
- toolInfo := &plugins.ToolInfo{
- Binaries: map[string]string{
- "semgrep": filepath.Join(homeDirectory, ".cache/codacy/tools/semgrep@1.78.0/venv/bin/semgrep"),
- },
- }
-
- // Specify files to analyze
- filesToAnalyze := []string{"sample.js"}
-
- // Run Semgrep analysis on specific files
- err = RunSemgrep(testDirectory, toolInfo.Binaries["semgrep"], filesToAnalyze, tempResultFile, "sarif")
- if err != nil {
- t.Fatalf("Failed to run semgrep on specific files: %v", err)
- }
-
- // Verify file exists and has content
- fileInfo, err := os.Stat(tempResultFile)
- assert.NoError(t, err, "Failed to stat output file")
- assert.Greater(t, fileInfo.Size(), int64(0), "Output file should not be empty")
-}
diff --git a/tools/trivyRunner_test.go b/tools/trivyRunner_test.go
deleted file mode 100644
index c7d1dd66..00000000
--- a/tools/trivyRunner_test.go
+++ /dev/null
@@ -1,56 +0,0 @@
-package tools
-
-import (
- "fmt"
- "log"
- "os"
- "path/filepath"
- "strings"
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestRunTrivyToFile(t *testing.T) {
- homeDirectory, err := os.UserHomeDir()
- if err != nil {
- log.Fatal(err.Error())
- }
- currentDirectory, err := os.Getwd()
- if err != nil {
- log.Fatal(err.Error())
- }
-
- testDirectory := "testdata/repositories/trivy"
- tempResultFile := filepath.Join(os.TempDir(), "trivy.sarif")
- defer os.Remove(tempResultFile)
-
- repositoryToAnalyze := filepath.Join(testDirectory, "src")
-
- trivyBinary := filepath.Join(homeDirectory, ".cache/codacy/tools/trivy@0.59.1/trivy")
-
- err = RunTrivy(repositoryToAnalyze, trivyBinary, nil, tempResultFile, "sarif")
- if err != nil {
- t.Fatalf("Failed to run trivy: %v", err)
- }
-
- // Check if the output file was created
- obtainedSarifBytes, err := os.ReadFile(tempResultFile)
- if err != nil {
- t.Fatalf("Failed to read output file: %v", err)
- }
- obtainedSarif := string(obtainedSarifBytes)
- filePrefix := "file://" + currentDirectory + "/"
- fmt.Println(filePrefix)
- actualSarif := strings.ReplaceAll(obtainedSarif, filePrefix, "")
-
- // Read the expected SARIF
- expectedSarifFile := filepath.Join(testDirectory, "expected.sarif")
- expectedSarifBytes, err := os.ReadFile(expectedSarifFile)
- if err != nil {
- log.Fatal(err)
- }
- expectedSarif := strings.TrimSpace(string(expectedSarifBytes))
-
- assert.Equal(t, expectedSarif, actualSarif, "output did not match expected")
-}